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 path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import type { ProjectConfig } from "./types";
export const TITLE_TEXT = ` export const TITLE_TEXT = `
╔════════════════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════════════════╗
@@ -26,3 +27,11 @@ export const TITLE_TEXT = `
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const distPath = path.dirname(__filename); const distPath = path.dirname(__filename);
export const PKG_ROOT = path.join(distPath, "../"); 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 fs from "fs-extra";
import ora from "ora"; import ora from "ora";
import { setupTurso } from "./helpers/db-setup"; import { setupTurso } from "./helpers/db-setup";
import type { ProjectOptions } from "./types"; import type { ProjectConfig } from "./types";
import { logger } from "./utils/logger"; 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 spinner = ora("Creating project directory...").start();
const projectDir = path.resolve(process.cwd(), options.projectName); const projectDir = path.resolve(process.cwd(), options.projectName);

View File

@@ -1,40 +1,38 @@
import { checkbox, confirm, input, select } from "@inquirer/prompts"; import { checkbox, confirm, input, select } from "@inquirer/prompts";
import chalk from "chalk"; import chalk from "chalk";
import { Command } from "commander"; import { Command } from "commander";
import { DEFAULT_CONFIG } from "./consts";
import { createProject } from "./create-project"; import { createProject } from "./create-project";
import { renderTitle } from "./render-title"; 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 { getVersion } from "./utils/get-version";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";
const program = new Command(); const program = new Command();
type CliOptions = { async function gatherConfig(
yes: boolean; flags: Partial<ProjectConfig>,
}; ): Promise<ProjectConfig> {
const config: ProjectConfig = {
async function main(options: CliOptions) { projectName: "",
try { database: "libsql",
renderTitle();
console.log(chalk.bold("\n🚀 Creating a new Better-T Stack project...\n"));
const defaults = {
projectName: "my-better-t-app",
database: "libsql" as ProjectDatabase,
auth: true, auth: true,
features: [] as ProjectFeature[], features: [],
git: true,
}; };
const projectName = options.yes config.projectName =
? defaults.projectName flags.projectName ??
: await input({ (await input({
message: "Project name:", message: "Project name:",
default: defaults.projectName, default: "my-better-t-app",
}); }));
const database = options.yes if (flags.database) {
? defaults.database config.database = flags.database;
: await select<ProjectDatabase>({ } else {
config.database = await select<ProjectDatabase>({
message: chalk.cyan("Select database:"), message: chalk.cyan("Select database:"),
choices: [ choices: [
{ {
@@ -51,17 +49,19 @@ async function main(options: CliOptions) {
}, },
], ],
}); });
}
const auth = options.yes config.auth =
? defaults.auth flags.auth ??
: await confirm({ (await confirm({
message: "Add authentication with Better-Auth?", message: "Add authentication with Better-Auth?",
default: defaults.auth, default: true,
}); }));
const features = options.yes if (flags.features) {
? defaults.features config.features = flags.features;
: await checkbox<ProjectFeature>({ } else {
config.features = await checkbox<ProjectFeature>({
message: chalk.cyan("Select additional features:"), message: chalk.cyan("Select additional features:"),
choices: [ choices: [
{ {
@@ -77,26 +77,61 @@ async function main(options: CliOptions) {
{ {
value: "SEO", value: "SEO",
name: "Basic SEO setup", name: "Basic SEO setup",
description: chalk.dim( description: chalk.dim("Search engine optimization configuration"),
"Search engine optimization configuration",
),
}, },
], ],
}); });
if (options.yes) {
logger.info("Using default values due to -y flag");
} }
const projectOptions = { return config;
projectName, }
git: true,
database, async function main() {
auth, try {
features, renderTitle();
logger.info("\n🚀 Creating a new Better-T Stack project...\n");
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[],
}; };
await createProject(projectOptions); const config = options.yes
? DEFAULT_CONFIG
: await gatherConfig(flagConfig);
if (options.yes) {
logger.info("Using default configuration");
logger.info(JSON.stringify(config, null, 2));
}
await createProject(config);
logger.info("\n📋 To reproduce this setup, run:");
logger.success(chalk.cyan(generateReproducibleCommand(config)));
} catch (error) { } catch (error) {
if (error instanceof Error && error.message.includes("User force closed")) { if (error instanceof Error && error.message.includes("User force closed")) {
console.log("\n"); console.log("\n");
@@ -114,11 +149,4 @@ process.on("SIGINT", () => {
process.exit(0); process.exit(0);
}); });
program main();
.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();

View File

@@ -1,7 +1,8 @@
export type ProjectFeature = "docker" | "github-actions" | "SEO"; export type ProjectFeature = "docker" | "github-actions" | "SEO";
export type ProjectDatabase = "libsql" | "postgres"; export type ProjectDatabase = "libsql" | "postgres";
export type ProjectOptions = { export type ProjectConfig = {
projectName: string; projectName: string;
git: boolean; git: boolean;
database: ProjectDatabase; 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(" ");
}