From b56096f36a71f53b43ca86b8c592736132540fa5 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 13 Feb 2025 01:52:34 +0530 Subject: [PATCH] feat(cli): add reproducible command output and flag support --- .changeset/stupid-elephants-type.md | 5 + apps/cli/src/consts.ts | 9 + apps/cli/src/create-project.ts | 4 +- apps/cli/src/index.ts | 200 ++++++++++-------- apps/cli/src/types.ts | 3 +- .../utils/generate-reproducible-command.ts | 30 +++ 6 files changed, 162 insertions(+), 89 deletions(-) create mode 100644 .changeset/stupid-elephants-type.md create mode 100644 apps/cli/src/utils/generate-reproducible-command.ts diff --git a/.changeset/stupid-elephants-type.md b/.changeset/stupid-elephants-type.md new file mode 100644 index 0000000..39c5cff --- /dev/null +++ b/.changeset/stupid-elephants-type.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +feat(cli): add reproducible command output and flag support diff --git a/apps/cli/src/consts.ts b/apps/cli/src/consts.ts index 5c1f352..90b01f1 100644 --- a/apps/cli/src/consts.ts +++ b/apps/cli/src/consts.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; +import type { ProjectConfig } from "./types"; export const TITLE_TEXT = ` ╔════════════════════════════════════════════════════════════╗ @@ -26,3 +27,11 @@ export const TITLE_TEXT = ` const __filename = fileURLToPath(import.meta.url); const distPath = path.dirname(__filename); export const PKG_ROOT = path.join(distPath, "../"); + +export const DEFAULT_CONFIG: ProjectConfig = { + projectName: "my-better-t-app", + database: "libsql", + auth: true, + features: [], + git: true, +}; diff --git a/apps/cli/src/create-project.ts b/apps/cli/src/create-project.ts index d2b8eb2..a5517c7 100644 --- a/apps/cli/src/create-project.ts +++ b/apps/cli/src/create-project.ts @@ -3,10 +3,10 @@ import { execa } from "execa"; import fs from "fs-extra"; import ora from "ora"; import { setupTurso } from "./helpers/db-setup"; -import type { ProjectOptions } from "./types"; +import type { ProjectConfig } from "./types"; import { logger } from "./utils/logger"; -export async function createProject(options: ProjectOptions) { +export async function createProject(options: ProjectConfig) { const spinner = ora("Creating project directory...").start(); const projectDir = path.resolve(process.cwd(), options.projectName); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 750da7f..cef614d 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,102 +1,137 @@ import { checkbox, confirm, input, select } from "@inquirer/prompts"; import chalk from "chalk"; import { Command } from "commander"; +import { DEFAULT_CONFIG } from "./consts"; import { createProject } from "./create-project"; import { renderTitle } from "./render-title"; -import type { ProjectDatabase, ProjectFeature } from "./types"; +import type { ProjectConfig, ProjectDatabase, ProjectFeature } from "./types"; +import { generateReproducibleCommand } from "./utils/generate-reproducible-command"; import { getVersion } from "./utils/get-version"; import { logger } from "./utils/logger"; const program = new Command(); -type CliOptions = { - yes: boolean; -}; +async function gatherConfig( + flags: Partial, +): Promise { + const config: ProjectConfig = { + projectName: "", + database: "libsql", + auth: true, + features: [], + git: true, + }; -async function main(options: CliOptions) { + config.projectName = + flags.projectName ?? + (await input({ + message: "Project name:", + default: "my-better-t-app", + })); + + if (flags.database) { + config.database = flags.database; + } else { + config.database = await select({ + message: chalk.cyan("Select database:"), + choices: [ + { + value: "libsql", + name: "libSQL", + description: chalk.dim( + "(Recommended) - Turso's embedded SQLite database", + ), + }, + { + value: "postgres", + name: "PostgreSQL", + description: chalk.dim("Traditional relational database"), + }, + ], + }); + } + + config.auth = + flags.auth ?? + (await confirm({ + message: "Add authentication with Better-Auth?", + default: true, + })); + + if (flags.features) { + config.features = flags.features; + } else { + config.features = await checkbox({ + message: chalk.cyan("Select additional features:"), + choices: [ + { + value: "docker", + name: "Docker setup", + description: chalk.dim("Containerize your application"), + }, + { + value: "github-actions", + name: "GitHub Actions", + description: chalk.dim("CI/CD workflows"), + }, + { + value: "SEO", + name: "Basic SEO setup", + description: chalk.dim("Search engine optimization configuration"), + }, + ], + }); + } + + return config; +} + +async function main() { try { renderTitle(); - console.log(chalk.bold("\n🚀 Creating a new Better-T Stack project...\n")); + logger.info("\n🚀 Creating a new Better-T Stack project...\n"); - const defaults = { - projectName: "my-better-t-app", - database: "libsql" as ProjectDatabase, - auth: true, - features: [] as ProjectFeature[], + program + .name("create-better-t-stack") + .description("Create a new Better-T Stack project") + .version(getVersion()) + .argument("[project-directory]", "Project name/directory") + .option("-y, --yes", "Use default configuration") + .option("--database ", "Database type (libsql or postgres)") + .option("--auth", "Include authentication") + .option("--no-auth", "Exclude authentication") + .option("--docker", "Include Docker setup") + .option("--github-actions", "Include GitHub Actions") + .option("--seo", "Include SEO setup") + .parse(); + + const options = program.opts(); + const projectDirectory = program.args[0]; + + const flagConfig: Partial = { + projectName: projectDirectory, + database: options.database as ProjectDatabase, + auth: options.auth, + features: [ + ...(options.docker ? ["docker"] : []), + ...(options.githubActions ? ["github-actions"] : []), + ...(options.seo ? ["SEO"] : []), + ] as ProjectFeature[], }; - const projectName = options.yes - ? defaults.projectName - : await input({ - message: "Project name:", - default: defaults.projectName, - }); - - const database = options.yes - ? defaults.database - : await select({ - message: chalk.cyan("Select database:"), - choices: [ - { - value: "libsql", - name: "libSQL", - description: chalk.dim( - "(Recommended) - Turso's embedded SQLite database", - ), - }, - { - value: "postgres", - name: "PostgreSQL", - description: chalk.dim("Traditional relational database"), - }, - ], - }); - - const auth = options.yes - ? defaults.auth - : await confirm({ - message: "Add authentication with Better-Auth?", - default: defaults.auth, - }); - - const features = options.yes - ? defaults.features - : await checkbox({ - message: chalk.cyan("Select additional features:"), - choices: [ - { - value: "docker", - name: "Docker setup", - description: chalk.dim("Containerize your application"), - }, - { - value: "github-actions", - name: "GitHub Actions", - description: chalk.dim("CI/CD workflows"), - }, - { - value: "SEO", - name: "Basic SEO setup", - description: chalk.dim( - "Search engine optimization configuration", - ), - }, - ], - }); + const config = options.yes + ? DEFAULT_CONFIG + : await gatherConfig(flagConfig); if (options.yes) { - logger.info("Using default values due to -y flag"); + logger.info("Using default configuration"); + logger.info(JSON.stringify(config, null, 2)); } - const projectOptions = { - projectName, - git: true, - database, - auth, - features, - }; + await createProject(config); - await createProject(projectOptions); + logger.info("\n📋 To reproduce this setup, run:"); + logger.success(chalk.cyan(generateReproducibleCommand(config))); } catch (error) { if (error instanceof Error && error.message.includes("User force closed")) { console.log("\n"); @@ -114,11 +149,4 @@ process.on("SIGINT", () => { process.exit(0); }); -program - .name("create-better-t-stack") - .description("Create a new Better-T Stack project") - .version(getVersion()) - .option("-y, --yes", "Accept all defaults") - .action((options) => main(options)); - -program.parse(); +main(); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 4a1c229..2b21eb9 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -1,7 +1,8 @@ export type ProjectFeature = "docker" | "github-actions" | "SEO"; + export type ProjectDatabase = "libsql" | "postgres"; -export type ProjectOptions = { +export type ProjectConfig = { projectName: string; git: boolean; database: ProjectDatabase; diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts new file mode 100644 index 0000000..1f89bfc --- /dev/null +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -0,0 +1,30 @@ +import { DEFAULT_CONFIG } from "../consts"; +import type { ProjectConfig } from "../types"; + +export function generateReproducibleCommand(config: ProjectConfig): string { + const parts = ["bunx create-better-t-stack"]; + + if (config.projectName !== DEFAULT_CONFIG.projectName) { + parts.push(config.projectName); + } + + if (config.database !== DEFAULT_CONFIG.database) { + parts.push(`--database ${config.database}`); + } + + if (config.auth !== DEFAULT_CONFIG.auth) { + parts.push(config.auth ? "--auth" : "--no-auth"); + } + + if (config.features.includes("docker")) { + parts.push("--docker"); + } + if (config.features.includes("github-actions")) { + parts.push("--github-actions"); + } + if (config.features.includes("SEO")) { + parts.push("--seo"); + } + + return parts.join(" "); +}