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,102 +1,137 @@
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 = {
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 { try {
renderTitle(); 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 = { program
projectName: "my-better-t-app", .name("create-better-t-stack")
database: "libsql" as ProjectDatabase, .description("Create a new Better-T Stack project")
auth: true, .version(getVersion())
features: [] as ProjectFeature[], .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 const config = options.yes
? defaults.projectName ? DEFAULT_CONFIG
: await input({ : await gatherConfig(flagConfig);
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",
),
},
],
});
if (options.yes) { 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 = { await createProject(config);
projectName,
git: true,
database,
auth,
features,
};
await createProject(projectOptions); 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(" ");
}