mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add reproducible command output and flag support
This commit is contained in:
5
.changeset/stupid-elephants-type.md
Normal file
5
.changeset/stupid-elephants-type.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(cli): add reproducible command output and flag support
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
30
apps/cli/src/utils/generate-reproducible-command.ts
Normal file
30
apps/cli/src/utils/generate-reproducible-command.ts
Normal 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(" ");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user