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:
5
.changeset/loud-socks-reflect.md
Normal file
5
.changeset/loud-socks-reflect.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add flags and prompt for orm, add none option in database
|
||||||
@@ -9,9 +9,9 @@ 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",
|
||||||
database: "sqlite",
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
auth: true,
|
auth: true,
|
||||||
features: [],
|
features: [],
|
||||||
git: true,
|
git: true,
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
orm: "drizzle",
|
|
||||||
};
|
};
|
||||||
@@ -52,6 +52,8 @@ export async function createProject(options: ProjectConfig) {
|
|||||||
|
|
||||||
if (options.database === "sqlite") {
|
if (options.database === "sqlite") {
|
||||||
await setupTurso(projectDir);
|
await setupTurso(projectDir);
|
||||||
|
} else if (options.database === "postgres") {
|
||||||
|
// Handle postgres setup
|
||||||
}
|
}
|
||||||
|
|
||||||
const installDepsResponse = await confirm({
|
const installDepsResponse = await confirm({
|
||||||
|
|||||||
@@ -1,30 +1,12 @@
|
|||||||
import path from "node:path";
|
import { cancel, intro, log, outro, spinner } from "@clack/prompts";
|
||||||
import {
|
|
||||||
cancel,
|
|
||||||
confirm,
|
|
||||||
group,
|
|
||||||
intro,
|
|
||||||
log,
|
|
||||||
multiselect,
|
|
||||||
outro,
|
|
||||||
select,
|
|
||||||
spinner,
|
|
||||||
text,
|
|
||||||
} from "@clack/prompts";
|
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import fs from "fs-extra";
|
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import { DEFAULT_CONFIG } from "./consts";
|
import { DEFAULT_CONFIG } from "./constants";
|
||||||
import { createProject } from "./helpers/create-project";
|
import { createProject } from "./helpers/create-project";
|
||||||
import type {
|
import { gatherConfig } from "./prompts/config-prompts";
|
||||||
PackageManager,
|
import type { PackageManager, ProjectConfig, ProjectFeature } from "./types";
|
||||||
ProjectConfig,
|
import { displayConfig } from "./utils/display-config";
|
||||||
ProjectDatabase,
|
|
||||||
ProjectFeature,
|
|
||||||
ProjectORM,
|
|
||||||
} from "./types";
|
|
||||||
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
||||||
import { getUserPkgManager } from "./utils/get-package-manager";
|
|
||||||
import { getVersion } from "./utils/get-version";
|
import { getVersion } from "./utils/get-version";
|
||||||
import { renderTitle } from "./utils/render-title";
|
import { renderTitle } from "./utils/render-title";
|
||||||
|
|
||||||
@@ -35,215 +17,6 @@ process.on("SIGINT", () => {
|
|||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
async function gatherConfig(
|
|
||||||
flags: Partial<ProjectConfig>,
|
|
||||||
): Promise<ProjectConfig> {
|
|
||||||
const result = await group(
|
|
||||||
{
|
|
||||||
projectName: async () => {
|
|
||||||
if (flags.projectName) return flags.projectName;
|
|
||||||
let isValid = false;
|
|
||||||
let projectName: string | symbol = "";
|
|
||||||
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: flags.projectName,
|
|
||||||
defaultValue: defaultName,
|
|
||||||
validate: (value) => {
|
|
||||||
const nameToUse = value.trim() || defaultName;
|
|
||||||
const projectDir = path.resolve(process.cwd(), nameToUse);
|
|
||||||
|
|
||||||
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 (typeof response === "symbol") {
|
|
||||||
cancel(pc.red("Operation cancelled."));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
projectName = response || defaultName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return projectName as string;
|
|
||||||
},
|
|
||||||
database: () =>
|
|
||||||
flags.database !== undefined
|
|
||||||
? Promise.resolve(flags.database)
|
|
||||||
: select<ProjectDatabase>({
|
|
||||||
message: "Which database would you like to use?",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "sqlite",
|
|
||||||
label: "SQLite",
|
|
||||||
hint: "by Turso (recommended)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "postgres",
|
|
||||||
label: "PostgreSQL",
|
|
||||||
hint: "Traditional relational database",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
orm: () =>
|
|
||||||
flags.orm !== undefined
|
|
||||||
? Promise.resolve(flags.orm)
|
|
||||||
: select<ProjectORM>({
|
|
||||||
message: "Which ORM would you like to use?",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "drizzle",
|
|
||||||
label: "Drizzle",
|
|
||||||
hint: "Type-safe, lightweight ORM (recommended)",
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// value: "prisma",
|
|
||||||
// label: "Prisma (coming soon)",
|
|
||||||
// hint: "Feature-rich ORM with great DX",
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
initialValue: "drizzle",
|
|
||||||
}),
|
|
||||||
auth: () =>
|
|
||||||
flags.auth !== undefined
|
|
||||||
? Promise.resolve(flags.auth)
|
|
||||||
: confirm({
|
|
||||||
message: "Would you like to add authentication with Better-Auth?",
|
|
||||||
initialValue: DEFAULT_CONFIG.auth,
|
|
||||||
}),
|
|
||||||
features: () =>
|
|
||||||
flags.features !== undefined
|
|
||||||
? Promise.resolve(flags.features)
|
|
||||||
: 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,
|
|
||||||
}),
|
|
||||||
git: () =>
|
|
||||||
flags.git !== undefined
|
|
||||||
? Promise.resolve(flags.git)
|
|
||||||
: confirm({
|
|
||||||
message: "Initialize a new git repository?",
|
|
||||||
initialValue: DEFAULT_CONFIG.git,
|
|
||||||
}),
|
|
||||||
packageManager: async () => {
|
|
||||||
if (flags.packageManager !== undefined) {
|
|
||||||
return flags.packageManager;
|
|
||||||
}
|
|
||||||
const detectedPackageManager = getUserPkgManager();
|
|
||||||
|
|
||||||
const useDetected = await confirm({
|
|
||||||
message: `Use ${detectedPackageManager} as your package manager?`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (useDetected) return detectedPackageManager;
|
|
||||||
|
|
||||||
return 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",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onCancel: () => {
|
|
||||||
cancel(pc.red("Operation cancelled."));
|
|
||||||
process.exit(0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
projectName: result.projectName ?? DEFAULT_CONFIG.projectName,
|
|
||||||
database: result.database ?? DEFAULT_CONFIG.database,
|
|
||||||
orm: result.orm ?? DEFAULT_CONFIG.orm,
|
|
||||||
auth: result.auth ?? DEFAULT_CONFIG.auth,
|
|
||||||
features: result.features ?? DEFAULT_CONFIG.features,
|
|
||||||
git: result.git ?? DEFAULT_CONFIG.git,
|
|
||||||
packageManager: result.packageManager ?? DEFAULT_CONFIG.packageManager,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayConfig(config: Partial<ProjectConfig>) {
|
|
||||||
const configDisplay = [];
|
|
||||||
|
|
||||||
if (config.projectName) {
|
|
||||||
configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
|
|
||||||
}
|
|
||||||
if (config.database) {
|
|
||||||
configDisplay.push(`${pc.blue("Database:")} ${config.database}`);
|
|
||||||
}
|
|
||||||
if (config.orm) {
|
|
||||||
configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`);
|
|
||||||
}
|
|
||||||
if (config.auth !== undefined) {
|
|
||||||
configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`);
|
|
||||||
}
|
|
||||||
if (config.features?.length) {
|
|
||||||
configDisplay.push(`${pc.blue("Features:")} ${config.features.join(", ")}`);
|
|
||||||
}
|
|
||||||
if (config.git !== undefined) {
|
|
||||||
configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`);
|
|
||||||
}
|
|
||||||
if (config.packageManager) {
|
|
||||||
configDisplay.push(
|
|
||||||
`${pc.blue("Package Manager:")} ${config.packageManager}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return configDisplay.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
try {
|
try {
|
||||||
@@ -256,6 +29,7 @@ async function main() {
|
|||||||
.version(getVersion())
|
.version(getVersion())
|
||||||
.argument("[project-directory]", "Project name/directory")
|
.argument("[project-directory]", "Project name/directory")
|
||||||
.option("-y, --yes", "Use default configuration")
|
.option("-y, --yes", "Use default configuration")
|
||||||
|
.option("--no-database", "Skip database setup")
|
||||||
.option("--sqlite", "Use SQLite database")
|
.option("--sqlite", "Use SQLite database")
|
||||||
.option("--postgres", "Use PostgreSQL database")
|
.option("--postgres", "Use PostgreSQL database")
|
||||||
.option("--auth", "Include authentication")
|
.option("--auth", "Include authentication")
|
||||||
@@ -277,38 +51,28 @@ async function main() {
|
|||||||
const projectDirectory = program.args[0];
|
const projectDirectory = program.args[0];
|
||||||
|
|
||||||
const flagConfig: Partial<ProjectConfig> = {
|
const flagConfig: Partial<ProjectConfig> = {
|
||||||
projectName: projectDirectory || undefined,
|
...(projectDirectory && { projectName: projectDirectory }),
|
||||||
database: options.sqlite
|
...(options.database === false && { database: "none" }),
|
||||||
? "sqlite"
|
...(options.sqlite && { database: "sqlite" }),
|
||||||
: options.postgres
|
...(options.postgres && { database: "postgres" }),
|
||||||
? "postgres"
|
...(options.drizzle && { orm: "drizzle" }),
|
||||||
: undefined,
|
...(options.prisma && { orm: "prisma" }),
|
||||||
orm: options.drizzle ? "drizzle" : options.prisma ? "prisma" : undefined,
|
...("auth" in options && { auth: options.auth }),
|
||||||
auth: "auth" in options ? options.auth : undefined,
|
...(options.npm && { packageManager: "npm" }),
|
||||||
packageManager: options.npm
|
...(options.pnpm && { packageManager: "pnpm" }),
|
||||||
? "npm"
|
...(options.yarn && { packageManager: "yarn" }),
|
||||||
: options.pnpm
|
...(options.bun && { packageManager: "bun" }),
|
||||||
? "pnpm"
|
...("git" in options && { git: options.git }),
|
||||||
: options.yarn
|
...((options.docker || options.githubActions || options.seo) && {
|
||||||
? "yarn"
|
features: [
|
||||||
: options.bun
|
...(options.docker ? ["docker"] : []),
|
||||||
? "bun"
|
...(options.githubActions ? ["github-actions"] : []),
|
||||||
: undefined,
|
...(options.seo ? ["SEO"] : []),
|
||||||
git: "git" in options ? options.git : undefined,
|
] as ProjectFeature[],
|
||||||
features:
|
}),
|
||||||
options.docker || options.githubActions || options.seo
|
|
||||||
? ([
|
|
||||||
...(options.docker ? ["docker"] : []),
|
|
||||||
...(options.githubActions ? ["github-actions"] : []),
|
|
||||||
...(options.seo ? ["SEO"] : []),
|
|
||||||
] as ProjectFeature[])
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (!options.yes && Object.keys(flagConfig).length > 0) {
|
||||||
!options.yes &&
|
|
||||||
Object.values(flagConfig).some((v) => v !== undefined)
|
|
||||||
) {
|
|
||||||
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(flagConfig));
|
||||||
log.message("");
|
log.message("");
|
||||||
@@ -317,23 +81,26 @@ async function main() {
|
|||||||
const config = options.yes
|
const config = options.yes
|
||||||
? {
|
? {
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
yes: true,
|
|
||||||
projectName: projectDirectory ?? DEFAULT_CONFIG.projectName,
|
projectName: projectDirectory ?? DEFAULT_CONFIG.projectName,
|
||||||
database: options.database ?? DEFAULT_CONFIG.database,
|
database:
|
||||||
orm: options.drizzle
|
options.database === false
|
||||||
? "drizzle"
|
? "none"
|
||||||
: options.prisma
|
: (options.database ?? DEFAULT_CONFIG.database),
|
||||||
? "prisma"
|
orm:
|
||||||
: DEFAULT_CONFIG.orm, // Add this line
|
options.database === false
|
||||||
|
? "none"
|
||||||
|
: options.drizzle
|
||||||
|
? "drizzle"
|
||||||
|
: options.prisma
|
||||||
|
? "prisma"
|
||||||
|
: DEFAULT_CONFIG.orm,
|
||||||
auth: options.auth ?? DEFAULT_CONFIG.auth,
|
auth: options.auth ?? DEFAULT_CONFIG.auth,
|
||||||
git: options.git ?? DEFAULT_CONFIG.git,
|
git: options.git ?? DEFAULT_CONFIG.git,
|
||||||
packageManager:
|
packageManager:
|
||||||
options.packageManager ?? DEFAULT_CONFIG.packageManager,
|
flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager,
|
||||||
features: [
|
features: flagConfig.features?.length
|
||||||
...(options.docker ? ["docker"] : []),
|
? flagConfig.features
|
||||||
...(options.githubActions ? ["github-actions"] : []),
|
: DEFAULT_CONFIG.features,
|
||||||
...(options.seo ? ["SEO"] : []),
|
|
||||||
] as ProjectFeature[],
|
|
||||||
}
|
}
|
||||||
: await gatherConfig(flagConfig);
|
: await gatherConfig(flagConfig);
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
export type ProjectFeature = "docker" | "github-actions" | "SEO";
|
export type ProjectFeature = "docker" | "github-actions" | "SEO";
|
||||||
|
|
||||||
export type ProjectDatabase = "sqlite" | "postgres";
|
export type ProjectDatabase = "sqlite" | "postgres" | "none";
|
||||||
|
|
||||||
export type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
|
export type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
|
||||||
|
|
||||||
export type ProjectORM = "drizzle" | "prisma";
|
export type ProjectORM = "drizzle" | "prisma" | "none";
|
||||||
|
|
||||||
export type ProjectConfig = {
|
export type ProjectConfig = {
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
|
|||||||
32
apps/cli/src/utils/display-config.ts
Normal file
32
apps/cli/src/utils/display-config.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import pc from "picocolors";
|
||||||
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
|
export function displayConfig(config: Partial<ProjectConfig>) {
|
||||||
|
const configDisplay = [];
|
||||||
|
|
||||||
|
if (config.projectName) {
|
||||||
|
configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
|
||||||
|
}
|
||||||
|
if (config.database) {
|
||||||
|
configDisplay.push(`${pc.blue("Database:")} ${config.database}`);
|
||||||
|
}
|
||||||
|
if (config.orm) {
|
||||||
|
configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`);
|
||||||
|
}
|
||||||
|
if (config.auth !== undefined) {
|
||||||
|
configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`);
|
||||||
|
}
|
||||||
|
if (config.features?.length) {
|
||||||
|
configDisplay.push(`${pc.blue("Features:")} ${config.features.join(", ")}`);
|
||||||
|
}
|
||||||
|
if (config.git !== undefined) {
|
||||||
|
configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`);
|
||||||
|
}
|
||||||
|
if (config.packageManager) {
|
||||||
|
configDisplay.push(
|
||||||
|
`${pc.blue("Package Manager:")} ${config.packageManager}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return configDisplay.join("\n");
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_CONFIG } from "../consts";
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export function generateReproducibleCommand(config: ProjectConfig): string {
|
export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||||
@@ -14,11 +14,17 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
|||||||
flags.push("-y");
|
flags.push("-y");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle database flag
|
||||||
if (config.database !== DEFAULT_CONFIG.database) {
|
if (config.database !== DEFAULT_CONFIG.database) {
|
||||||
flags.push(config.database === "sqlite" ? "--sqlite" : "--postgres");
|
if (config.database === "none") {
|
||||||
|
flags.push("--no-database");
|
||||||
|
} else {
|
||||||
|
flags.push(config.database === "sqlite" ? "--sqlite" : "--postgres");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.orm !== DEFAULT_CONFIG.orm) {
|
// Handle ORM flag only if database is not "none"
|
||||||
|
if (config.database !== "none" && config.orm !== DEFAULT_CONFIG.orm) {
|
||||||
flags.push(config.orm === "drizzle" ? "--drizzle" : "--prisma");
|
flags.push(config.orm === "drizzle" ? "--drizzle" : "--prisma");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import { PKG_ROOT } from "../consts";
|
import { PKG_ROOT } from "../constants";
|
||||||
|
|
||||||
export const getVersion = () => {
|
export const getVersion = () => {
|
||||||
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
const packageJsonPath = path.join(PKG_ROOT, "package.json");
|
||||||
|
|||||||
Reference in New Issue
Block a user