mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
organize code
This commit is contained in:
23
apps/cli/src/prompts/auth.ts
Normal file
23
apps/cli/src/prompts/auth.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
|
||||
export async function getAuthChoice(
|
||||
auth: boolean | undefined,
|
||||
hasDatabase: boolean,
|
||||
): Promise<boolean> {
|
||||
if (!hasDatabase) return false;
|
||||
if (auth !== undefined) return auth;
|
||||
|
||||
const response = await confirm({
|
||||
message: "Would you like to add authentication with Better-Auth?",
|
||||
initialValue: DEFAULT_CONFIG.auth,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
62
apps/cli/src/prompts/config-prompts.ts
Normal file
62
apps/cli/src/prompts/config-prompts.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { cancel, group } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
ProjectDatabase,
|
||||
ProjectFeature,
|
||||
ProjectORM,
|
||||
} from "../types";
|
||||
import { getAuthChoice } from "./auth";
|
||||
import { getDatabaseChoice } from "./database";
|
||||
import { getFeaturesChoice } from "./features";
|
||||
import { getGitChoice } from "./git";
|
||||
import { getORMChoice } from "./orm";
|
||||
import { getPackageManagerChoice } from "./package-manager";
|
||||
import { getProjectName } from "./project-name";
|
||||
|
||||
interface PromptGroupResults {
|
||||
projectName: string;
|
||||
database: ProjectDatabase;
|
||||
orm: ProjectORM;
|
||||
auth: boolean;
|
||||
features: ProjectFeature[];
|
||||
git: boolean;
|
||||
packageManager: PackageManager;
|
||||
}
|
||||
|
||||
export async function gatherConfig(
|
||||
flags: Partial<ProjectConfig>,
|
||||
): Promise<ProjectConfig> {
|
||||
const result = await group<PromptGroupResults>(
|
||||
{
|
||||
projectName: async () => {
|
||||
return getProjectName(flags.projectName);
|
||||
},
|
||||
database: () => getDatabaseChoice(flags.database),
|
||||
orm: ({ results }) =>
|
||||
getORMChoice(flags.orm, results.database !== "none"),
|
||||
auth: ({ results }) =>
|
||||
getAuthChoice(flags.auth, results.database !== "none"),
|
||||
features: () => getFeaturesChoice(flags.features),
|
||||
git: () => getGitChoice(flags.git),
|
||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
projectName: result.projectName,
|
||||
database: result.database,
|
||||
orm: result.orm,
|
||||
auth: result.auth,
|
||||
features: result.features,
|
||||
git: result.git,
|
||||
packageManager: result.packageManager,
|
||||
};
|
||||
}
|
||||
38
apps/cli/src/prompts/database.ts
Normal file
38
apps/cli/src/prompts/database.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectDatabase } from "../types";
|
||||
|
||||
export async function getDatabaseChoice(
|
||||
database?: ProjectDatabase,
|
||||
): Promise<ProjectDatabase> {
|
||||
if (database !== undefined) return database;
|
||||
|
||||
const response = await select<ProjectDatabase>({
|
||||
message: "Which database would you like to use?",
|
||||
options: [
|
||||
{
|
||||
value: "none",
|
||||
label: "None",
|
||||
hint: "No database setup",
|
||||
},
|
||||
{
|
||||
value: "sqlite",
|
||||
label: "SQLite",
|
||||
hint: "by Turso (recommended)",
|
||||
},
|
||||
{
|
||||
value: "postgres",
|
||||
label: "PostgreSQL",
|
||||
hint: "Traditional relational database",
|
||||
},
|
||||
],
|
||||
initialValue: "sqlite",
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
38
apps/cli/src/prompts/features.ts
Normal file
38
apps/cli/src/prompts/features.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectFeature } from "../types";
|
||||
|
||||
export async function getFeaturesChoice(
|
||||
features?: ProjectFeature[],
|
||||
): Promise<ProjectFeature[]> {
|
||||
if (features !== undefined) return features;
|
||||
|
||||
const response = await multiselect<ProjectFeature>({
|
||||
message: "Which features would you like to add?",
|
||||
options: [
|
||||
{
|
||||
value: "docker",
|
||||
label: "Docker setup",
|
||||
hint: "Containerize your application",
|
||||
},
|
||||
{
|
||||
value: "github-actions",
|
||||
label: "GitHub Actions",
|
||||
hint: "CI/CD workflows",
|
||||
},
|
||||
{
|
||||
value: "SEO",
|
||||
label: "Basic SEO setup",
|
||||
hint: "Search engine optimization configuration",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
19
apps/cli/src/prompts/git.ts
Normal file
19
apps/cli/src/prompts/git.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
|
||||
export async function getGitChoice(git?: boolean): Promise<boolean> {
|
||||
if (git !== undefined) return git;
|
||||
|
||||
const response = await confirm({
|
||||
message: "Initialize a new git repository?",
|
||||
initialValue: DEFAULT_CONFIG.git,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
30
apps/cli/src/prompts/orm.ts
Normal file
30
apps/cli/src/prompts/orm.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectORM } from "../types";
|
||||
|
||||
export async function getORMChoice(
|
||||
orm: ProjectORM | undefined,
|
||||
hasDatabase: boolean,
|
||||
): Promise<ProjectORM> {
|
||||
if (!hasDatabase) return "none";
|
||||
if (orm !== undefined) return orm;
|
||||
|
||||
const response = await select<ProjectORM>({
|
||||
message: "Which ORM would you like to use?",
|
||||
options: [
|
||||
{
|
||||
value: "drizzle",
|
||||
label: "Drizzle",
|
||||
hint: "Type-safe, lightweight ORM (recommended)",
|
||||
},
|
||||
],
|
||||
initialValue: "drizzle",
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
52
apps/cli/src/prompts/package-manager.ts
Normal file
52
apps/cli/src/prompts/package-manager.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { cancel, confirm, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager } from "../types";
|
||||
import { getUserPkgManager } from "../utils/get-package-manager";
|
||||
|
||||
export async function getPackageManagerChoice(
|
||||
packageManager?: PackageManager,
|
||||
): Promise<PackageManager> {
|
||||
if (packageManager !== undefined) return packageManager;
|
||||
|
||||
const detectedPackageManager = getUserPkgManager();
|
||||
const useDetected = await confirm({
|
||||
message: `Use ${detectedPackageManager} as your package manager?`,
|
||||
});
|
||||
|
||||
if (isCancel(useDetected)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (useDetected) return detectedPackageManager;
|
||||
|
||||
const response = await select<PackageManager>({
|
||||
message: "Which package manager would you like to use?",
|
||||
options: [
|
||||
{ value: "npm", label: "npm", hint: "Node Package Manager" },
|
||||
{
|
||||
value: "pnpm",
|
||||
label: "pnpm",
|
||||
hint: "Fast, disk space efficient package manager",
|
||||
},
|
||||
{
|
||||
value: "yarn",
|
||||
label: "yarn",
|
||||
hint: "Fast, reliable, and secure dependency management",
|
||||
},
|
||||
{
|
||||
value: "bun",
|
||||
label: "bun",
|
||||
hint: "All-in-one JavaScript runtime & toolkit (recommended)",
|
||||
},
|
||||
],
|
||||
initialValue: "bun",
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
95
apps/cli/src/prompts/project-name.ts
Normal file
95
apps/cli/src/prompts/project-name.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, text } from "@clack/prompts";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
|
||||
const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"];
|
||||
|
||||
const MAX_LENGTH = 255;
|
||||
|
||||
function validateDirectoryName(name: string): string | undefined {
|
||||
if (!name) return "Project name cannot be empty";
|
||||
if (name.length > MAX_LENGTH) {
|
||||
return `Project name must be less than ${MAX_LENGTH} characters`;
|
||||
}
|
||||
if (INVALID_CHARS.some((char) => name.includes(char))) {
|
||||
return "Project name contains invalid characters";
|
||||
}
|
||||
if (name.startsWith(".") || name.startsWith("-")) {
|
||||
return "Project name cannot start with a dot or dash";
|
||||
}
|
||||
if (
|
||||
name.toLowerCase() === "node_modules" ||
|
||||
name.toLowerCase() === "favicon.ico"
|
||||
) {
|
||||
return "Project name is reserved";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getProjectName(initialName?: string): Promise<string> {
|
||||
if (initialName) {
|
||||
const finalDirName = path.basename(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isValid = false;
|
||||
let projectName = "";
|
||||
let defaultName = DEFAULT_CONFIG.projectName;
|
||||
let counter = 1;
|
||||
|
||||
while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName))) {
|
||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
while (!isValid) {
|
||||
const response = await text({
|
||||
message: "What is your project named? (directory name or path)",
|
||||
placeholder: defaultName,
|
||||
initialValue: initialName,
|
||||
defaultValue: defaultName,
|
||||
validate: (value) => {
|
||||
const nameToUse = value.trim() || defaultName;
|
||||
|
||||
const finalDirName = path.basename(nameToUse);
|
||||
const validationError = validateDirectoryName(finalDirName);
|
||||
if (validationError) return validationError;
|
||||
|
||||
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.`;
|
||||
}
|
||||
}
|
||||
|
||||
isValid = true;
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
projectName = response || defaultName;
|
||||
}
|
||||
|
||||
return projectName;
|
||||
}
|
||||
Reference in New Issue
Block a user