feat(cli): add reproducible command output and flag support

This commit is contained in:
Aman Varshney
2025-02-13 01:52:34 +05:30
parent 7476afb21c
commit b56096f36a
6 changed files with 162 additions and 89 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
feat(cli): add reproducible command output and flag support

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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<ProjectConfig>,
): Promise<ProjectConfig> {
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<ProjectDatabase>({
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<ProjectFeature>({
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 <type>", "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<ProjectConfig> = {
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<ProjectDatabase>({
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<ProjectFeature>({
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();

View File

@@ -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;

View File

@@ -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(" ");
}