diff --git a/.changeset/weak-places-check.md b/.changeset/weak-places-check.md new file mode 100644 index 0000000..0dbcc88 --- /dev/null +++ b/.changeset/weak-places-check.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +fix cli project name check diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index f899535..571eb6e 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,11 +1,14 @@ +import path from "node:path"; import { cancel, intro, log, outro } from "@clack/prompts"; import { consola } from "consola"; +import fs from "fs-extra"; import pc from "picocolors"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { DEFAULT_CONFIG } from "./constants"; import { createProject } from "./helpers/create-project"; import { gatherConfig } from "./prompts/config-prompts"; +import { getProjectName } from "./prompts/project-name"; import type { ProjectAddons, ProjectApi, @@ -155,18 +158,28 @@ async function main() { log.message(""); } - const config = options.yes - ? { - ...DEFAULT_CONFIG, - projectName: projectDirectory ?? DEFAULT_CONFIG.projectName, - ...flagConfig, - } - : await gatherConfig(flagConfig); - + let config: ProjectConfig; if (options.yes) { - log.info(pc.yellow("Using these default options:")); + config = { + ...DEFAULT_CONFIG, + projectName: projectDirectory ?? DEFAULT_CONFIG.projectName, + ...flagConfig, + }; + log.info(pc.yellow("Using these default/flag options:")); log.message(displayConfig(config)); log.message(""); + } else { + config = await gatherConfig(flagConfig); + } + + const projectDir = path.resolve(process.cwd(), config.projectName); + + if ( + fs.pathExistsSync(projectDir) && + fs.readdirSync(projectDir).length > 0 + ) { + const newProjectName = await getProjectName(); + config.projectName = newProjectName; } await createProject(config); @@ -210,7 +223,6 @@ function processAndValidateFlags( ): Partial { const config: Partial = {}; - // --- Database and ORM validation --- if (options.database) { config.database = options.database as ProjectDatabase; } @@ -388,7 +400,6 @@ function processAndValidateFlags( } } - // --- Addons validation --- if (options.addons && options.addons.length > 0) { if (options.addons.includes("none")) { if (options.addons.length > 1) { @@ -433,7 +444,6 @@ function processAndValidateFlags( } } - // --- Examples validation --- if (options.examples && options.examples.length > 0) { if (options.examples.includes("none")) { if (options.examples.length > 1) { @@ -483,7 +493,6 @@ function processAndValidateFlags( } } - // --- Other flags --- if (options.packageManager) { config.packageManager = options.packageManager as ProjectPackageManager; } diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index 7a1a84a..e1d8de5 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -100,7 +100,7 @@ const StackArchitect = ({ } }, []); - // biome-ignore lint/correctness/useExhaustiveDependencies: + // biome-ignore lint/correctness/useExhaustiveDependencies: Intentionally managing complex dependencies useEffect(() => { let changed = false; const nextStack = { ...stack }; @@ -371,6 +371,12 @@ const StackArchitect = ({ "MongoDB Atlas setup requires the MongoDB database.", ); } + + if (stack.orm !== "prisma") { + notes.dbSetup.push( + "MongoDB Atlas setup requires the Prisma ORM (implicitly selected).", + ); + } } else if (stack.dbSetup === "neon") { if (stack.database !== "postgres") { notes.dbSetup.push("Neon setup requires the PostgreSQL database."); @@ -394,6 +400,11 @@ const StackArchitect = ({ if (stack.database === "mongodb" && stack.orm === "drizzle") { notes.database.push("MongoDB is only compatible with the Prisma ORM."); } + if (stack.dbSetup !== "none") { + notes.database.push( + `Changing the database might reset the '${TECH_OPTIONS.dbSetup.find((db) => db.id === stack.dbSetup)?.name}' setup if it becomes incompatible.`, + ); + } notes.orm = []; if (stack.database === "none") { @@ -445,7 +456,7 @@ const StackArchitect = ({ const currentStack = { ...prev }; if (category === "frontend") { - let currentSelection = [...currentStack.frontend]; + const currentSelection = [...currentStack.frontend]; const webTypes = [ "tanstack-router", "react-router", @@ -464,25 +475,25 @@ const StackArchitect = ({ return prev; } - if (currentSelection.includes(techId)) { - currentSelection = currentSelection.filter((id) => id !== techId); - - if (currentSelection.length === 0) { - } - return { ...currentStack, frontend: currentSelection }; - } - let newSelection = [...currentSelection]; if (newSelection.includes("none")) { newSelection = newSelection.filter((id) => id !== "none"); } - if (webTypes.includes(techId)) { - newSelection = newSelection.filter((id) => !webTypes.includes(id)); - } + if (newSelection.includes(techId)) { + newSelection = newSelection.filter((id) => id !== techId); - newSelection.push(techId); + if (newSelection.length === 0) { + } + } else { + if (webTypes.includes(techId)) { + newSelection = newSelection.filter( + (id) => !webTypes.includes(id), + ); + } + newSelection.push(techId); + } return { ...currentStack, frontend: newSelection }; } @@ -490,9 +501,12 @@ const StackArchitect = ({ if (category === "addons" || category === "examples") { const currentArray = [...(currentStack[category] || [])]; const index = currentArray.indexOf(techId); + const nextArray = [...currentArray]; if (index >= 0) { - currentArray.splice(index, 1); + nextArray.splice(index, 1); + if (techId === "biome" && nextArray.includes("husky")) { + } } else { if (category === "examples") { if (!hasWebFrontend && (techId === "todo" || techId === "ai")) @@ -506,27 +520,56 @@ const StackArchitect = ({ (techId === "pwa" || techId === "tauri") ) return prev; - if (techId === "husky" && !currentArray.includes("biome")) { - currentArray.push("biome"); + if (techId === "husky" && !nextArray.includes("biome")) { + nextArray.push("biome"); } } - currentArray.push(techId); + nextArray.push(techId); } - return { ...currentStack, [category]: currentArray }; + return { ...currentStack, [category]: nextArray }; } if (category === "database") { if (currentStack.database === techId) return prev; - const updatedState = { ...currentStack, database: techId }; + const updatedState: Partial = { database: techId }; + const currentDbSetup = currentStack.dbSetup; + + let resetDbSetup = false; + if (currentDbSetup === "turso" && techId !== "sqlite") + resetDbSetup = true; + if (currentDbSetup === "prisma-postgres" && techId !== "postgres") + resetDbSetup = true; + if (currentDbSetup === "mongodb-atlas" && techId !== "mongodb") + resetDbSetup = true; + if (currentDbSetup === "neon" && techId !== "postgres") + resetDbSetup = true; + if (techId === "none") resetDbSetup = true; + + if (resetDbSetup && currentDbSetup !== "none") { + updatedState.dbSetup = "none"; + } if (techId === "none") { - } else if (prev.database === "none") { - updatedState.orm = techId === "mongodb" ? "prisma" : "drizzle"; - updatedState.dbSetup = "none"; + updatedState.orm = "none"; + updatedState.auth = "false"; } else { + if (prev.database === "none") { + updatedState.orm = techId === "mongodb" ? "prisma" : "drizzle"; + } else { + if (techId === "mongodb" && currentStack.orm === "drizzle") { + updatedState.orm = "prisma"; + } else if ( + techId !== "mongodb" && + currentStack.orm === "prisma" && + currentDbSetup === "turso" && + techId === "sqlite" + ) { + } + } } - return updatedState; + + return { ...currentStack, ...updatedState }; } if (category === "orm") { @@ -541,7 +584,6 @@ const StackArchitect = ({ return prev; if (currentStack.orm === techId) return prev; - return { ...currentStack, orm: techId }; } @@ -549,8 +591,19 @@ const StackArchitect = ({ if (currentStack.database === "none" && techId !== "none") return prev; - if (currentStack.dbSetup === techId) return prev; + if (techId === "turso") { + if (currentStack.database !== "sqlite") return prev; + if (currentStack.orm === "prisma") return prev; + } else if (techId === "prisma-postgres") { + if (currentStack.database !== "postgres") return prev; + if (currentStack.orm !== "prisma") return prev; + } else if (techId === "mongodb-atlas") { + if (currentStack.database !== "mongodb") return prev; + } else if (techId === "neon") { + if (currentStack.database !== "postgres") return prev; + } + if (currentStack.dbSetup === techId) return prev; return { ...currentStack, dbSetup: techId }; } @@ -575,7 +628,19 @@ const StackArchitect = ({ category === "install" ) { if (currentStack[category] === techId) return prev; - return { ...currentStack, [category]: techId }; + + const updatedState: Partial = { [category]: techId }; + if (category === "backendFramework" && techId === "elysia") { + const currentExamples = currentStack.examples || []; + if (currentExamples.includes("ai")) { + updatedState.examples = currentExamples.filter( + (ex) => ex !== "ai", + ); + } + } else if (category === "backendFramework" && techId === "hono") { + } + + return { ...currentStack, ...updatedState }; } return prev; @@ -606,10 +671,14 @@ const StackArchitect = ({ if (saveButton && saveTextSpan) { const originalText = saveTextSpan.textContent; saveTextSpan.textContent = "Saved!"; + saveButton.classList.add("bg-green-200", "dark:bg-green-800/70"); + saveButton.classList.remove("bg-green-100", "dark:bg-green-900/50"); setTimeout(() => { if (saveTextSpan.textContent === "Saved!") { saveTextSpan.textContent = originalText; + saveButton.classList.remove("bg-green-200", "dark:bg-green-800/70"); + saveButton.classList.add("bg-green-100", "dark:bg-green-900/50"); } }, 2000); } @@ -621,6 +690,7 @@ const StackArchitect = ({ setProjectNameError( validateProjectName(lastSavedStack.projectName || ""), ); + setActiveTab("frontend"); } }, [lastSavedStack]); @@ -632,6 +702,7 @@ const StackArchitect = ({ setStack(preset.stack); setProjectNameError(validateProjectName(preset.stack.projectName || "")); setShowPresets(false); + setActiveTab("frontend"); } }, []); @@ -650,15 +721,25 @@ const StackArchitect = ({ techId === "prisma" && stack.dbSetup === "turso" ) - return "Prisma ORM is not compatible with Turso."; + return "Prisma ORM is not compatible with Turso DB setup (requires Drizzle)."; } if (category === "dbSetup" && techId !== "none") { if (stack.database === "none") return "Select a database before choosing a cloud setup."; if (techId === "turso") { - if (stack.orm === "prisma") - return "Turso is not compatible with the Prisma ORM."; + if (stack.database !== "sqlite") + return "Turso requires SQLite database."; + if (stack.orm === "prisma") return "Turso requires Drizzle ORM."; + } else if (techId === "prisma-postgres") { + if (stack.database !== "postgres") + return "Requires PostgreSQL database."; + if (stack.orm !== "prisma") return "Requires Prisma ORM."; + } else if (techId === "mongodb-atlas") { + if (stack.database !== "mongodb") return "Requires MongoDB database."; + } else if (techId === "neon") { + if (stack.database !== "postgres") + return "Requires PostgreSQL database."; } } if ( @@ -716,9 +797,11 @@ const StackArchitect = ({
+
Stack Architect Terminal
+
)} + {showPresets && (

@@ -855,7 +939,7 @@ const StackArchitect = ({

@@ -928,9 +1013,11 @@ const StackArchitect = ({ (tech) => { let isSelected = false; if (activeTab === "addons" || activeTab === "examples") { - isSelected = stack[activeTab].includes(tech.id); + isSelected = ( + stack[activeTab as "addons" | "examples"] || [] + ).includes(tech.id); } else if (activeTab === "frontend") { - isSelected = stack.frontend.includes(tech.id); + isSelected = (stack.frontend || []).includes(tech.id); } else { isSelected = stack[activeTab as keyof StackState] === tech.id; @@ -1015,149 +1102,38 @@ const StackArchitect = ({
- {stack.frontend - .map((id) => TECH_OPTIONS.frontend.find((f) => f.id === id)) - .filter(Boolean) - .map((tech) => ( - - {tech?.icon} {tech?.name} - - ))} - {TECH_OPTIONS.runtime.find((t) => t.id === stack.runtime) && ( - - { - TECH_OPTIONS.runtime.find((t) => t.id === stack.runtime) - ?.icon - }{" "} - { - TECH_OPTIONS.runtime.find((t) => t.id === stack.runtime) - ?.name - } - - )} - {TECH_OPTIONS.backendFramework.find( - (t) => t.id === stack.backendFramework, - ) && ( - - { - TECH_OPTIONS.backendFramework.find( - (t) => t.id === stack.backendFramework, - )?.icon - }{" "} - { - TECH_OPTIONS.backendFramework.find( - (t) => t.id === stack.backendFramework, - )?.name - } - - )} - {TECH_OPTIONS.api.find((t) => t.id === stack.api) && ( - - {TECH_OPTIONS.api.find((t) => t.id === stack.api)?.icon}{" "} - {TECH_OPTIONS.api.find((t) => t.id === stack.api)?.name} - - )} - {TECH_OPTIONS.database.find((t) => t.id === stack.database) && - stack.database !== "none" && ( - - { - TECH_OPTIONS.database.find( - (t) => t.id === stack.database, - )?.icon - }{" "} - { - TECH_OPTIONS.database.find( - (t) => t.id === stack.database, - )?.name - } - - )} - {TECH_OPTIONS.orm.find((t) => t.id === stack.orm) && - stack.orm !== "none" && ( - - {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "} - {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name} - - )} - {TECH_OPTIONS.auth.find((t) => t.id === stack.auth) && - stack.auth === "true" && ( - - {TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.icon}{" "} - {TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name} - - )} - {TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup) && - stack.dbSetup !== "none" && ( - - { - TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup) - ?.icon - }{" "} - { - TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup) - ?.name - } - - )} - {stack.addons - .map((id) => TECH_OPTIONS.addons.find((a) => a.id === id)) - .filter(Boolean) - .map((tech) => ( - - {tech?.icon} {tech?.name} - - ))} - {stack.examples - .map((id) => TECH_OPTIONS.examples.find((e) => e.id === id)) - .filter(Boolean) - .map((tech) => ( - - {tech?.icon} {tech?.name} - - ))} - {TECH_OPTIONS.packageManager.find( - (t) => t.id === stack.packageManager, - ) && ( - - { - TECH_OPTIONS.packageManager.find( - (t) => t.id === stack.packageManager, - )?.icon - }{" "} - { - TECH_OPTIONS.packageManager.find( - (t) => t.id === stack.packageManager, - )?.name - } - - )} - {TECH_OPTIONS.git.find((t) => t.id === stack.git) && ( - - {TECH_OPTIONS.git.find((t) => t.id === stack.git)?.icon}{" "} - {TECH_OPTIONS.git.find((t) => t.id === stack.git)?.name} - - )} - {TECH_OPTIONS.install.find((t) => t.id === stack.install) && ( - - { - TECH_OPTIONS.install.find((t) => t.id === stack.install) - ?.icon - }{" "} - { - TECH_OPTIONS.install.find((t) => t.id === stack.install) - ?.name - } - - )} + {Object.entries(TECH_OPTIONS).flatMap(([category, options]) => { + const categoryKey = category as keyof StackState; + const selectedValue = stack[categoryKey]; + + if (Array.isArray(selectedValue)) { + return selectedValue + .map((id) => options.find((opt) => opt.id === id)) + .filter(Boolean) + .map((tech) => ( + + {tech?.icon} {tech?.name} + + )); + } + const tech = options.find((opt) => opt.id === selectedValue); + + if (tech && tech.id !== "none" && tech.id !== "false") { + return ( + + {tech.icon} {tech.name} + + ); + } + + return []; + })}
@@ -1204,4 +1180,37 @@ const StackArchitect = ({ ); }; +const getBadgeColors = (category: string): string => { + switch (category) { + case "frontend": + return "border-blue-300 bg-blue-100 text-blue-800 dark:border-blue-700/30 dark:bg-blue-900/30 dark:text-blue-300"; + case "runtime": + return "border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700/30 dark:bg-amber-900/30 dark:text-amber-300"; + case "backendFramework": + return "border-blue-300 bg-blue-100 text-blue-800 dark:border-blue-700/30 dark:bg-blue-900/30 dark:text-blue-300"; + case "api": + return "border-indigo-300 bg-indigo-100 text-indigo-800 dark:border-indigo-700/30 dark:bg-indigo-900/30 dark:text-indigo-300"; + case "database": + return "border-indigo-300 bg-indigo-100 text-indigo-800 dark:border-indigo-700/30 dark:bg-indigo-900/30 dark:text-indigo-300"; + case "orm": + return "border-cyan-300 bg-cyan-100 text-cyan-800 dark:border-cyan-700/30 dark:bg-cyan-900/30 dark:text-cyan-300"; + case "auth": + return "border-green-300 bg-green-100 text-green-800 dark:border-green-700/30 dark:bg-green-900/30 dark:text-green-300"; + case "dbSetup": + return "border-pink-300 bg-pink-100 text-pink-800 dark:border-pink-700/30 dark:bg-pink-900/30 dark:text-pink-300"; + case "addons": + return "border-violet-300 bg-violet-100 text-violet-800 dark:border-violet-700/30 dark:bg-violet-900/30 dark:text-violet-300"; + case "examples": + return "border-teal-300 bg-teal-100 text-teal-800 dark:border-teal-700/30 dark:bg-teal-900/30 dark:text-teal-300"; + case "packageManager": + return "border-orange-300 bg-orange-100 text-orange-800 dark:border-orange-700/30 dark:bg-orange-900/30 dark:text-orange-300"; + case "git": + return "border-gray-300 bg-gray-100 text-gray-800 dark:border-gray-700/30 dark:bg-gray-900/30 dark:text-gray-300"; + case "install": + return "border-lime-300 bg-lime-100 text-lime-800 dark:border-lime-700/30 dark:bg-lime-900/30 dark:text-lime-300"; + default: + return "border-gray-300 bg-gray-100 text-gray-800 dark:border-gray-700/30 dark:bg-gray-900/30 dark:text-gray-300"; + } +}; + export default StackArchitect; diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index eb8f25d..a2604bd 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -175,13 +175,6 @@ export const TECH_OPTIONS = { icon: "◮", color: "from-purple-400 to-purple-600", }, - { - id: "none", - name: "No ORM", - description: "Skip ORM integration", - icon: "🚫", - color: "from-gray-400 to-gray-600", - }, ], dbSetup: [ { diff --git a/bun.lock b/bun.lock index 65cb7cd..51767c0 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.0.0", + "version": "2.0.1", "bin": { "create-better-t-stack": "dist/index.js", },