use spaces instead of commas in flags

This commit is contained in:
Aman Varshney
2025-04-01 14:57:46 +05:30
parent 47e429554d
commit b296ac28ed
14 changed files with 390 additions and 163 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
use spaces instead of commas in flags

View File

@@ -1,6 +1,6 @@
# Create Better-T-Stack CLI
A CLI tool for scaffolding type-safe full-stack apps with Hono/Elysia backends, React web frontends, and Expo native apps, all connected through tRPC.
A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations
## Quick Start
@@ -49,10 +49,10 @@ Options:
--orm <type> ORM type (none, drizzle, prisma)
--auth Include authentication
--no-auth Exclude authentication
--frontend <types> Frontend types (web,native or both)
--addons <types> Additional addons (pwa,tauri,biome,husky)
--frontend <types...> Frontend types (web, native, none)
--addons <types...> Additional addons (pwa, tauri, biome, husky)
--no-addons Skip all additional addons
--examples <types> Examples to include (todo,ai)
--examples <types...> Examples to include (todo, ai)
--no-examples Skip all examples
--git Initialize git repository
--no-git Skip git initialization
@@ -75,7 +75,7 @@ npx create-better-t-stack my-app -y
Create a project with specific options:
```bash
npx create-better-t-stack my-app --database postgres --orm drizzle --auth --addons pwa,biome
npx create-better-t-stack my-app --database postgres --orm drizzle --auth --addons pwa biome
```
Create a project with Elysia and Node.js runtime:
@@ -85,10 +85,10 @@ npx create-better-t-stack my-app --backend elysia --runtime node
Create a project with specific frontend options:
```bash
npx create-better-t-stack my-app --frontend web,native
npx create-better-t-stack my-app --frontend web native
```
Create a project with examples:
```bash
npx create-better-t-stack my-app --examples todo,ai
npx create-better-t-stack my-app --examples todo ai
```

View File

@@ -7,10 +7,7 @@
"bin": {
"create-better-t-stack": "dist/index.js"
},
"files": [
"dist",
"template"
],
"files": ["dist", "template"],
"keywords": [],
"repository": {
"type": "git",

View File

@@ -10,6 +10,7 @@ import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup";
import { setupEnvironmentVariables } from "./env-setup";
import { setupExamples } from "./examples-setup";
import { installDependencies } from "./install-dependencies";
import { displayPostInstallInstructions } from "./post-installation";
import { initializeGit, updatePackageConfigurations } from "./project-config";
import { setupRuntime } from "./runtime-setup";
@@ -91,6 +92,14 @@ export async function createProject(options: ProjectConfig): Promise<string> {
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
if (!options.noInstall) {
await installDependencies({
projectDir,
packageManager: options.packageManager,
addons: options.addons,
});
}
displayPostInstallInstructions(
options.database,
options.projectName,

View File

@@ -81,7 +81,6 @@ app.post("/ai", async (c) => {
return stream(c, (stream) => stream.pipe(result.toDataStream()));
});`;
// Add the import section
if (indexContent.includes("import {")) {
const lastImportIndex = indexContent.lastIndexOf("import");
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
@@ -94,7 +93,6 @@ ${indexContent.substring(endOfLastImport + 1)}`;
${indexContent}`;
}
// Add the route handler
const trpcHandlerIndex =
indexContent.indexOf('app.use("/trpc"') ||
indexContent.indexOf("app.use(trpc(");
@@ -103,7 +101,6 @@ ${indexContent}`;
${indexContent.substring(trpcHandlerIndex)}`;
} else {
// Add it near the end before export
const exportIndex = indexContent.indexOf("export default");
if (exportIndex !== -1) {
indexContent = `${indexContent.substring(0, exportIndex)}${aiRouteHandler}

View File

@@ -3,13 +3,18 @@ import { Command } from "commander";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "./constants";
import { createProject } from "./helpers/create-project";
import { installDependencies } from "./helpers/install-dependencies";
import { gatherConfig } from "./prompts/config-prompts";
import type {
CLIOptions,
ProjectAddons,
ProjectBackend,
ProjectConfig,
ProjectDatabase,
ProjectExamples,
ProjectFrontend,
ProjectOrm,
ProjectPackageManager,
ProjectRuntime,
} from "./types";
import { displayConfig } from "./utils/display-config";
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
@@ -36,22 +41,13 @@ async function main() {
.option("--orm <type>", "ORM type (none, drizzle, prisma)")
.option("--auth", "Include authentication")
.option("--no-auth", "Exclude authentication")
.option("--frontend <types...>", "Frontend types (web, native, none)")
.option(
"--frontend <types>",
"Frontend types (web,native or both)",
(val) => val.split(",") as ProjectFrontend[],
)
.option(
"--addons <types>",
"Additional addons (pwa,tauri,biome,husky)",
(val) => val.split(",") as ProjectAddons[],
"--addons <types...>",
"Additional addons (pwa, tauri, biome, husky)",
)
.option("--no-addons", "Skip all additional addons")
.option(
"--examples <types>",
"Examples to include (todo,ai)",
(val) => val.split(",") as ProjectExamples[],
)
.option("--examples <types...>", "Examples to include (todo, ai)")
.option("--no-examples", "Skip all examples")
.option("--git", "Initialize git repository")
.option("--no-git", "Skip git initialization")
@@ -70,93 +66,12 @@ async function main() {
renderTitle();
intro(pc.magenta("Creating a new Better-T-Stack project"));
const options = program.opts();
const options = program.opts() as CLIOptions;
const projectDirectory = program.args[0];
if (
options.database &&
!["none", "sqlite", "postgres"].includes(options.database)
) {
cancel(
pc.red(
`Invalid database type: ${options.database}. Must be none, sqlite, or postgres.`,
),
);
process.exit(1);
}
validateOptions(options);
if (options.orm && !["none", "drizzle", "prisma"].includes(options.orm)) {
cancel(
pc.red(
`Invalid ORM type: ${options.orm}. Must be none, drizzle, or prisma.`,
),
);
process.exit(1);
}
if (
options.packageManager &&
!["npm", "pnpm", "bun"].includes(options.packageManager)
) {
cancel(
pc.red(
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
),
);
process.exit(1);
}
if (options.backend && !["hono", "elysia"].includes(options.backend)) {
cancel(
pc.red(
`Invalid backend framework: ${options.backend}. Must be hono or elysia.`,
),
);
process.exit(1);
}
if (options.runtime && !["bun", "node"].includes(options.runtime)) {
cancel(
pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`),
);
process.exit(1);
}
if (options.examples && options.examples.length > 0) {
const validExamples = ["todo", "ai"];
const invalidExamples = options.examples.filter(
(example: ProjectExamples) => !validExamples.includes(example),
);
if (invalidExamples.length > 0) {
cancel(
pc.red(
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
),
);
process.exit(1);
}
}
const flagConfig: Partial<ProjectConfig> = {
...(projectDirectory && { projectName: projectDirectory }),
...(options.database && { database: options.database }),
...(options.orm && { orm: options.orm }),
...("auth" in options && { auth: options.auth }),
...(options.packageManager && { packageManager: options.packageManager }),
...("git" in options && { git: options.git }),
...("install" in options && { noInstall: !options.install }),
...("turso" in options && { turso: options.turso }),
...(options.backend && { backend: options.backend }),
...(options.runtime && { runtime: options.runtime }),
...(options.frontend && { frontend: options.frontend }),
...((options.addons || options.addons === false) && {
addons: options.addons === false ? [] : options.addons,
}),
...((options.examples || options.examples === false) && {
examples: options.examples === false ? [] : options.examples,
}),
};
const flagConfig = processFlags(options, projectDirectory);
if (!options.yes && Object.keys(flagConfig).length > 0) {
log.info(pc.yellow("Using these pre-selected options:"));
@@ -178,15 +93,7 @@ async function main() {
log.message("");
}
const projectDir = await createProject(config);
if (!config.noInstall) {
await installDependencies({
projectDir,
packageManager: config.packageManager,
addons: config.addons,
});
}
await createProject(config);
log.success(
pc.blue(
@@ -211,6 +118,322 @@ async function main() {
}
}
function validateOptions(options: CLIOptions): void {
if (
options.database &&
!["none", "sqlite", "postgres"].includes(options.database)
) {
cancel(
pc.red(
`Invalid database type: ${options.database}. Must be none, sqlite, or postgres.`,
),
);
process.exit(1);
}
if (options.orm && !["none", "drizzle", "prisma"].includes(options.orm)) {
cancel(
pc.red(
`Invalid ORM type: ${options.orm}. Must be none, drizzle, or prisma.`,
),
);
process.exit(1);
}
if (options.database === "none") {
if (options.auth === true) {
cancel(
pc.red(
"Authentication requires a database. Cannot use --auth with --database none.",
),
);
process.exit(1);
}
if (options.orm && options.orm !== "none") {
cancel(
pc.red(
`Cannot use ORM with no database. Cannot use --orm ${options.orm} with --database none.`,
),
);
process.exit(1);
}
if ("turso" in options && options.turso === true) {
cancel(
pc.red(
"Turso setup requires a SQLite database. Cannot use --turso with --database none.",
),
);
process.exit(1);
}
}
if (
"turso" in options &&
options.turso === true &&
options.database &&
options.database !== "sqlite"
) {
cancel(
pc.red(
`Turso setup requires a SQLite database. Cannot use --turso with --database ${options.database}`,
),
);
process.exit(1);
}
if (
"turso" in options &&
options.turso === true &&
options.orm === "prisma"
) {
cancel(
pc.red(
"Turso setup is not compatible with Prisma. Cannot use --turso with --orm prisma",
),
);
process.exit(1);
}
if (
options.packageManager &&
!["npm", "pnpm", "bun"].includes(options.packageManager)
) {
cancel(
pc.red(
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
),
);
process.exit(1);
}
if (options.backend && !["hono", "elysia"].includes(options.backend)) {
cancel(
pc.red(
`Invalid backend framework: ${options.backend}. Must be hono or elysia.`,
),
);
process.exit(1);
}
if (options.runtime && !["bun", "node"].includes(options.runtime)) {
cancel(pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`));
process.exit(1);
}
if (
options.examples &&
Array.isArray(options.examples) &&
options.examples.length > 0
) {
const validExamples = ["todo", "ai"];
const invalidExamples = options.examples.filter(
(example: string) => !validExamples.includes(example),
);
if (invalidExamples.length > 0) {
cancel(
pc.red(
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
),
);
process.exit(1);
}
if (options.examples.includes("ai") && options.backend === "elysia") {
cancel(
pc.red(
"AI example is only compatible with Hono backend. Cannot use --examples ai with --backend elysia",
),
);
process.exit(1);
}
if (
options.frontend &&
!options.frontend.includes("web") &&
!options.frontend.includes("none")
) {
cancel(
pc.red(
"Examples require a web frontend. Cannot use --examples with --frontend native only",
),
);
process.exit(1);
}
}
if (options.frontend && options.frontend.length > 0) {
const validFrontends = ["web", "native", "none"];
const invalidFrontends = options.frontend.filter(
(frontend: string) => !validFrontends.includes(frontend),
);
if (invalidFrontends.length > 0) {
cancel(
pc.red(
`Invalid frontend(s): ${invalidFrontends.join(", ")}. Valid options are: ${validFrontends.join(", ")}.`,
),
);
process.exit(1);
}
if (options.frontend.includes("none") && options.frontend.length > 1) {
cancel(pc.red(`Cannot combine 'none' with other frontend options.`));
process.exit(1);
}
}
if (options.addons && options.addons.length > 0) {
const validAddons = ["pwa", "tauri", "biome", "husky"];
const invalidAddons = options.addons.filter(
(addon: string) => !validAddons.includes(addon),
);
if (invalidAddons.length > 0) {
cancel(
pc.red(
`Invalid addon(s): ${invalidAddons.join(", ")}. Valid options are: ${validAddons.join(", ")}.`,
),
);
process.exit(1);
}
const webSpecificAddons = ["pwa", "tauri"];
const hasWebSpecificAddons = options.addons.some((addon) =>
webSpecificAddons.includes(addon),
);
if (
hasWebSpecificAddons &&
options.frontend &&
!options.frontend.includes("web") &&
!options.frontend.includes("none")
) {
cancel(
pc.red(
`PWA and Tauri addons require a web frontend. Cannot use --addons ${options.addons
.filter((a) => webSpecificAddons.includes(a))
.join(", ")} with --frontend native only`,
),
);
process.exit(1);
}
}
}
function processFlags(
options: CLIOptions,
projectDirectory?: string,
): Partial<ProjectConfig> {
let frontend: ProjectFrontend[] | undefined = undefined;
if (options.frontend) {
if (options.frontend.includes("none")) {
frontend = [];
} else {
frontend = options.frontend.filter(
(f): f is ProjectFrontend => f === "web" || f === "native",
);
}
}
const database = options.database as ProjectDatabase | undefined;
let orm: ProjectOrm | undefined;
if (options.orm) {
orm = options.orm as ProjectOrm;
}
let auth: boolean | undefined = "auth" in options ? options.auth : undefined;
let tursoOption: boolean | undefined =
"turso" in options ? options.turso : undefined;
if (database === "none") {
orm = "none";
auth = false;
tursoOption = false;
}
let examples: ProjectExamples[] | undefined;
if ("examples" in options) {
if (options.examples === false) {
examples = [];
} else if (Array.isArray(options.examples)) {
examples = options.examples.filter(
(ex): ex is ProjectExamples => ex === "todo" || ex === "ai",
);
if (frontend && frontend.length > 0 && !frontend.includes("web")) {
examples = [];
log.warn(
pc.yellow("Examples require web frontend - ignoring examples flag"),
);
}
if (examples.includes("ai") && options.backend === "elysia") {
examples = examples.filter((ex) => ex !== "ai");
log.warn(
pc.yellow(
"AI example is not compatible with Elysia - removing AI example",
),
);
}
}
}
let addons: ProjectAddons[] | undefined;
if ("addons" in options) {
if (options.addons === undefined) {
addons = [];
} else if (options.addons) {
addons = options.addons.filter(
(addon): addon is ProjectAddons =>
addon === "pwa" ||
addon === "tauri" ||
addon === "biome" ||
addon === "husky",
);
if (frontend && frontend.length > 0 && !frontend.includes("web")) {
addons = addons.filter((addon) => !["pwa", "tauri"].includes(addon));
if (addons.length !== options.addons.length) {
log.warn(
pc.yellow(
"PWA and Tauri addons require web frontend - removing these addons",
),
);
}
}
if (addons.includes("husky") && !addons.includes("biome")) {
addons.push("biome");
}
}
}
const backend = options.backend as ProjectBackend | undefined;
const runtime = options.runtime as ProjectRuntime | undefined;
const packageManager = options.packageManager as
| ProjectPackageManager
| undefined;
return {
...(projectDirectory && { projectName: projectDirectory }),
...(database !== undefined && { database }),
...(orm !== undefined && { orm }),
...(auth !== undefined && { auth }),
...(packageManager && { packageManager }),
...("git" in options && { git: options.git }),
...("install" in options && { noInstall: !options.install }),
...(tursoOption !== undefined && { turso: tursoOption }),
...(backend && { backend }),
...(runtime && { runtime }),
...(frontend !== undefined && { frontend }),
...(addons !== undefined && { addons }),
...(examples !== undefined && { examples }),
};
}
main().catch((err) => {
log.error("Aborting installation...");
if (err instanceof Error) {

View File

@@ -22,3 +22,19 @@ export interface ProjectConfig {
turso?: boolean;
frontend: ProjectFrontend[];
}
export type CLIOptions = {
yes?: boolean;
database?: string;
orm?: string;
auth?: boolean;
frontend?: string[];
addons?: string[];
examples?: string[] | boolean;
git?: boolean;
packageManager?: string;
install?: boolean;
turso?: boolean;
backend?: string;
runtime?: string;
};

View File

@@ -32,17 +32,17 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
}
if (config.frontend && config.frontend.length > 0) {
flags.push(`--frontend ${config.frontend.join(",")}`);
flags.push(`--frontend ${config.frontend.join(" ")}`);
}
if (config.addons && config.addons.length > 0) {
flags.push(`--addons ${config.addons.join(",")}`);
flags.push(`--addons ${config.addons.join(" ")}`);
} else {
flags.push("--no-addons");
}
if (config.examples && config.examples.length > 0) {
flags.push(`--examples ${config.examples.join(",")}`);
flags.push(`--examples ${config.examples.join(" ")}`);
} else {
flags.push("--no-examples");
}

View File

@@ -1,18 +1,18 @@
export const NAV_THEME = {
light: {
background: "hsl(0 0% 100%)", // background
border: "hsl(240 5.9% 90%)", // border
card: "hsl(0 0% 100%)", // card
notification: "hsl(0 84.2% 60.2%)", // destructive
primary: "hsl(240 5.9% 10%)", // primary
text: "hsl(240 10% 3.9%)", // foreground
background: "hsl(0 0% 100%)",
border: "hsl(240 5.9% 90%)",
card: "hsl(0 0% 100%)",
notification: "hsl(0 84.2% 60.2%)",
primary: "hsl(240 5.9% 10%)",
text: "hsl(240 10% 3.9%)",
},
dark: {
background: "hsl(240 10% 3.9%)", // background
border: "hsl(240 3.7% 15.9%)", // border
card: "hsl(240 10% 3.9%)", // card
notification: "hsl(0 72% 51%)", // destructive
primary: "hsl(0 0% 98%)", // primary
text: "hsl(0 0% 98%)", // foreground
background: "hsl(240 10% 3.9%)",
border: "hsl(240 3.7% 15.9%)",
card: "hsl(240 10% 3.9%)",
notification: "hsl(0 72% 51%)",
primary: "hsl(0 0% 98%)",
text: "hsl(0 0% 98%)",
},
};

View File

@@ -19,9 +19,8 @@ const app = new Elysia()
const { request } = context;
if (["POST", "GET"].includes(request.method)) {
return auth.handler(request);
} else {
context.error(405);
}
context.error(405);
})
.all("/trpc/*", async (context) => {
const res = await fetchRequestHandler({
@@ -34,5 +33,5 @@ const app = new Elysia()
})
.get("/", () => "OK")
.listen(3000, () => {
console.log(`Server is running on http://localhost:3000`);
console.log("Server is running on http://localhost:3000");
});

View File

@@ -393,72 +393,58 @@ const StackArchitect = () => {
}
const projectName = stackState.projectName || "my-better-t-app";
const flags: string[] = ["--yes"]; // Start with yes flag
const flags: string[] = ["--yes"];
// Only add flags that differ from defaults
// Frontend (default is web)
if (stackState.frontend.length === 1 && stackState.frontend[0] === "none") {
flags.push("--frontend none");
} else if (
!(stackState.frontend.length === 1 && stackState.frontend[0] === "web")
) {
flags.push(`--frontend ${stackState.frontend.join(",")}`);
flags.push(`--frontend ${stackState.frontend.join(" ")}`);
}
// Database (default is sqlite)
if (stackState.database !== "sqlite") {
flags.push(`--database ${stackState.database}`);
}
// ORM (default is drizzle)
if (stackState.database !== "none" && stackState.orm !== "drizzle") {
flags.push(`--orm ${stackState.orm}`);
}
// Auth (default is true)
if (stackState.auth === "false") {
flags.push("--no-auth");
}
// Turso (default is false)
if (stackState.turso === "true") {
flags.push("--turso");
}
// Backend (default is hono)
if (stackState.backendFramework !== "hono") {
flags.push(`--backend ${stackState.backendFramework}`);
}
// Runtime (default is bun)
if (stackState.runtime !== "bun") {
flags.push(`--runtime ${stackState.runtime}`);
}
// Package manager (default is bun)
if (stackState.packageManager !== "bun") {
flags.push(`--package-manager ${stackState.packageManager}`);
}
// Git (default is true)
if (stackState.git === "false") {
flags.push("--no-git");
}
// Install (default is true)
if (stackState.install === "false") {
flags.push("--no-install");
}
// Addons (default is none)
if (stackState.addons.length > 0) {
flags.push(`--addons ${stackState.addons.join(",")}`);
flags.push(`--addons ${stackState.addons.join(" ")}`);
}
// Examples (default is none)
if (stackState.examples.length > 0) {
flags.push(`--examples ${stackState.examples.join(",")}`);
flags.push(`--examples ${stackState.examples.join(" ")}`);
}
return `${base} ${projectName} ${flags.join(" ")}`;
@@ -486,7 +472,6 @@ const StackArchitect = () => {
if (currentSelection.includes(techId)) {
if (currentSelection.length === 1) {
// Don't remove the last frontend option
return prev;
}
@@ -512,9 +497,7 @@ const StackArchitect = () => {
};
}
// Adding a frontend option
if (currentSelection.includes("none")) {
// Replace "none" with the selected option
return {
...prev,
frontend: [techId],

View File

@@ -42,7 +42,6 @@ export default function Testimonials() {
return () => window.removeEventListener("resize", handleResize);
}, []);
// Get visible tweets
const getVisibleTweets = () => {
const visible = [];
for (let i = 0; i < tweetsPerPage; i++) {

View File

@@ -9,7 +9,6 @@ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
*/
export const baseOptions: BaseLayoutProps = {
nav: {
// can be JSX too!
title: "Better-T-Stack",
enabled: false,
},

View File

@@ -22,7 +22,7 @@
"typescript": "5.7.3"
},
"lint-staged": {
"*": ["biome check --write ."]
"*": ["bun biome check --write ."]
},
"engines": {
"node": ">=20"