feat(cli): add vibe rules addon (#481)

This commit is contained in:
Aman Varshney
2025-08-09 12:06:23 +05:30
committed by GitHub
parent 9005a432cf
commit 6cf476a21e
42 changed files with 429 additions and 302 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
add vibe-rules addon with a better t stack rules file

2
.gitignore vendored
View File

@@ -37,3 +37,5 @@ yarn-error.log*
*.pem
.vscode
.env*.local
.smoke

View File

@@ -135,6 +135,7 @@ export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
turborepo: [],
starlight: [],
ultracite: [],
"vibe-rules": [],
oxlint: [],
fumadocs: [],
none: [],

View File

@@ -1,10 +1,11 @@
import path from "node:path";
import { cancel, isCancel, log, select, spinner, text } from "@clack/prompts";
import { isCancel, log, select, spinner, text } from "@clack/prompts";
import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { PackageManager, ProjectConfig } from "../../types";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import {
addEnvVariablesToFile,
@@ -177,10 +178,7 @@ export async function setupNeonPostgres(config: ProjectConfig) {
initialValue: "neondb",
});
if (isCancel(setupMethod)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
if (setupMethod === "neondb") {
await setupWithNeonDb(projectDir, packageManager);
@@ -198,10 +196,8 @@ export async function setupNeonPostgres(config: ProjectConfig) {
initialValue: NEON_REGIONS[0].value,
});
if (isCancel(projectName) || isCancel(regionId)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(projectName) || isCancel(regionId))
return exitCancelled("Operation cancelled");
const neonConfig = await createNeonProject(
projectName as string,

View File

@@ -1,11 +1,12 @@
import path from "node:path";
import { cancel, isCancel, log, select, text } from "@clack/prompts";
import { isCancel, log, select, text } from "@clack/prompts";
import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ORM, PackageManager, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import {
addEnvVariablesToFile,
@@ -60,10 +61,7 @@ async function setupWithCreateDb(
},
});
if (isCancel(databaseUrl)) {
cancel("Database setup cancelled");
return null;
}
if (isCancel(databaseUrl)) return null;
return {
databaseUrl: databaseUrl as string,
@@ -115,10 +113,7 @@ async function initPrismaDatabase(
},
});
if (isCancel(databaseUrl)) {
cancel("Database setup cancelled");
return null;
}
if (isCancel(databaseUrl)) return null;
return {
databaseUrl: databaseUrl as string,
@@ -245,10 +240,7 @@ export async function setupPrismaPostgres(config: ProjectConfig) {
initialValue: "create-db",
});
if (isCancel(setupMethod)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
let prismaConfig: PrismaConfig | null = null;

View File

@@ -1,19 +1,12 @@
import os from "node:os";
import path from "node:path";
import {
cancel,
confirm,
isCancel,
log,
select,
spinner,
text,
} from "@clack/prompts";
import { confirm, isCancel, log, select, spinner, text } from "@clack/prompts";
import consola from "consola";
import { $ } from "execa";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { commandExists } from "../../utils/command-exists";
import { exitCancelled } from "../../utils/errors";
import {
addEnvVariablesToFile,
type EnvVariable,
@@ -129,10 +122,7 @@ async function selectTursoGroup(): Promise<string | null> {
options: groupOptions,
});
if (isCancel(selectedGroup)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
return selectedGroup as string;
}
@@ -236,10 +226,7 @@ export async function setupTurso(config: ProjectConfig) {
initialValue: true,
});
if (isCancel(shouldInstall)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
if (!shouldInstall) {
await writeEnvFile(projectDir);
@@ -269,10 +256,7 @@ export async function setupTurso(config: ProjectConfig) {
placeholder: suggestedName,
});
if (isCancel(dbNameResponse)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
dbName = dbNameResponse as string;

View File

@@ -1,9 +1,10 @@
import path from "node:path";
import { cancel, log } from "@clack/prompts";
import { log } from "@clack/prompts";
import pc from "picocolors";
import type { AddInput, Addons, ProjectConfig } from "../../types";
import { validateAddonCompatibility } from "../../utils/addon-compatibility";
import { updateBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { setupAddons } from "../setup/addons-setup";
import {
detectProjectConfig,
@@ -12,11 +13,6 @@ import {
import { installDependencies } from "./install-dependencies";
import { setupAddonsTemplate } from "./template-manager";
function exitWithError(message: string): never {
cancel(pc.red(message));
process.exit(1);
}
export async function addAddonsToProject(
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
) {
@@ -71,10 +67,6 @@ export async function addAddonsToProject(
}
}
log.info(
`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`,
);
await setupAddonsTemplate(projectDir, config);
await setupAddons(config, true);

View File

@@ -1,8 +1,9 @@
import path from "node:path";
import { cancel, log } from "@clack/prompts";
import { log } from "@clack/prompts";
import pc from "picocolors";
import type { AddInput, ProjectConfig, WebDeploy } from "../../types";
import { updateBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { setupWebDeploy } from "../setup/web-deploy-setup";
import {
detectProjectConfig,
@@ -11,11 +12,6 @@ import {
import { installDependencies } from "./install-dependencies";
import { setupDeploymentTemplates } from "./template-manager";
function exitWithError(message: string): never {
cancel(pc.red(message));
process.exit(1);
}
export async function addDeploymentToProject(
input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean },
) {

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import { cancel, intro, log, outro } from "@clack/prompts";
import { intro, log, outro } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../../constants";
@@ -10,6 +10,7 @@ import { getDeploymentToAdd } from "../../prompts/web-deploy";
import type { AddInput, CreateInput, ProjectConfig } from "../../types";
import { trackProjectCreation } from "../../utils/analytics";
import { displayConfig } from "../../utils/display-config";
import { exitWithError, handleError } from "../../utils/errors";
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
import {
handleDirectoryConflict,
@@ -131,8 +132,7 @@ export async function createProjectHandler(
),
);
} catch (error) {
console.error(error);
process.exit(1);
handleError(error, "Failed to create project");
}
}
@@ -142,12 +142,9 @@ export async function addAddonsHandler(input: AddInput) {
const detectedConfig = await detectProjectConfig(projectDir);
if (!detectedConfig) {
cancel(
pc.red(
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
),
exitWithError(
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
);
process.exit(1);
}
if (!input.addons || input.addons.length === 0) {
@@ -215,7 +212,6 @@ export async function addAddonsHandler(input: AddInput) {
outro("Add command completed successfully!");
} catch (error) {
console.error(error);
process.exit(1);
handleError(error, "Failed to add addons or deployment");
}
}

View File

@@ -1,8 +1,8 @@
import { cancel, log } from "@clack/prompts";
import { log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { writeBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { setupAddons } from "../setup/addons-setup";
import { setupApi } from "../setup/api-setup";
import { setupAuth } from "../setup/auth-setup";
@@ -104,13 +104,11 @@ export async function createProject(options: ProjectConfig) {
return projectDir;
} catch (error) {
if (error instanceof Error) {
cancel(pc.red(`Error during project creation: ${error.message}`));
console.error(error.stack);
process.exit(1);
exitWithError(`Error during project creation: ${error.message}`);
} else {
cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
console.error(error);
process.exit(1);
exitWithError(`An unexpected error occurred: ${String(error)}`);
}
}
}

View File

@@ -535,6 +535,8 @@ export async function setupAddonsTemplate(
for (const addon of context.addons) {
if (addon === "none") continue;
if (addon === "vibe-rules") continue;
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
let addonDestDir = projectDir;

View File

@@ -10,6 +10,7 @@ import { setupFumadocs } from "./fumadocs-setup";
import { setupStarlight } from "./starlight-setup";
import { setupTauri } from "./tauri-setup";
import { setupUltracite } from "./ultracite-setup";
import { setupVibeRules } from "./vibe-rules-setup";
import { addPwaToViteConfig } from "./vite-pwa-setup";
export async function setupAddons(config: ProjectConfig, isAddCommand = false) {
@@ -85,6 +86,10 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
if (addons.includes("starlight")) {
await setupStarlight(config);
}
if (addons.includes("vibe-rules")) {
await setupVibeRules(config);
}
if (addons.includes("fumadocs")) {
await setupFumadocs(config);
}

View File

@@ -1,10 +1,11 @@
import path from "node:path";
import { cancel, isCancel, log, select } from "@clack/prompts";
import { isCancel, log, select } from "@clack/prompts";
import consola from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionCommand } from "../../utils/package-runner";
type FumadocsTemplate =
@@ -52,10 +53,7 @@ export async function setupFumadocs(config: ProjectConfig) {
initialValue: "next-mdx",
});
if (isCancel(template)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(template)) return exitCancelled("Operation cancelled");
const templateArg = TEMPLATES[template].value;

View File

@@ -1,8 +1,9 @@
import { cancel, isCancel, log, multiselect } from "@clack/prompts";
import { isCancel, log, multiselect } from "@clack/prompts";
import { execa } from "execa";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import { setupBiome } from "./addons-setup";
@@ -71,10 +72,7 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
required: false,
});
if (isCancel(editors)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(editors)) return exitCancelled("Operation cancelled");
const rules = await multiselect<UltraciteRule>({
message: "Choose rules",
@@ -86,10 +84,7 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
required: false,
});
if (isCancel(rules)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(rules)) return exitCancelled("Operation cancelled");
const ultraciteArgs = ["init", "--pm", packageManager];

View File

@@ -0,0 +1,112 @@
import path from "node:path";
import { isCancel, log, multiselect, spinner } from "@clack/prompts";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import { PKG_ROOT } from "../../constants";
import type { ProjectConfig } from "../../types";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import { processTemplate } from "../../utils/template-processor";
export async function setupVibeRules(config: ProjectConfig) {
const { packageManager, projectDir } = config;
try {
log.info("Setting up vibe-rules...");
const rulesDir = path.join(projectDir, ".bts");
const ruleFile = path.join(rulesDir, "rules.md");
if (!(await fs.pathExists(ruleFile))) {
const templatePath = path.join(
PKG_ROOT,
"templates",
"addons",
"vibe-rules",
".bts",
"rules.md.hbs",
);
if (await fs.pathExists(templatePath)) {
await fs.ensureDir(rulesDir);
await processTemplate(templatePath, ruleFile, config);
} else {
log.error(pc.red("Rules template not found for vibe-rules addon"));
return;
}
}
const EDITORS = {
cursor: { label: "Cursor", hint: ".cursor/rules/*.mdc" },
windsurf: { label: "Windsurf", hint: ".windsurfrules" },
"claude-code": { label: "Claude Code", hint: "CLAUDE.md" },
vscode: {
label: "VSCode",
hint: ".github/instructions/*.instructions.md",
},
gemini: { label: "Gemini", hint: "GEMINI.md" },
codex: { label: "Codex", hint: "AGENTS.md" },
clinerules: { label: "Cline/Roo", hint: ".clinerules/*.md" },
roo: { label: "Roo", hint: ".clinerules/*.md" },
zed: { label: "Zed", hint: ".rules/*.md" },
unified: { label: "Unified", hint: ".rules/*.md" },
} as const;
const selectedEditors = await multiselect<keyof typeof EDITORS>({
message: "Choose editors to install BTS rule",
options: Object.entries(EDITORS).map(([key, v]) => ({
value: key as keyof typeof EDITORS,
label: v.label,
hint: v.hint,
})),
required: false,
});
if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
const editorsArg = selectedEditors.join(", ");
const s = spinner();
s.start("Saving and applying BTS rules...");
try {
const saveCmd = getPackageExecutionCommand(
packageManager,
`vibe-rules@latest save bts -f ${JSON.stringify(
path.relative(projectDir, ruleFile),
)}`,
);
await execa(saveCmd, {
cwd: projectDir,
env: { CI: "true" },
shell: true,
});
for (const editor of selectedEditors) {
const loadCmd = getPackageExecutionCommand(
packageManager,
`vibe-rules@latest load bts ${editor}`,
);
await execa(loadCmd, {
cwd: projectDir,
env: { CI: "true" },
shell: true,
});
}
s.stop(`Applied BTS rules to: ${editorsArg}`);
} catch (error) {
s.stop(pc.red("Failed to apply BTS rules"));
throw error;
}
try {
await fs.remove(rulesDir);
} catch (_) {}
log.success("vibe-rules setup successfully!");
} catch (error) {
log.error(pc.red("Failed to set up vibe-rules"));
if (error instanceof Error) {
console.error(pc.red(error.message));
}
}
}

View File

@@ -1,5 +1,4 @@
import { intro, log } from "@clack/prompts";
import { consola } from "consola";
import pc from "picocolors";
import { createCli, trpcServer } from "trpc-cli";
import z from "zod";
@@ -21,6 +20,7 @@ import {
RuntimeSchema,
WebDeploySchema,
} from "./types";
import { handleError } from "./utils/errors";
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
import { openUrl } from "./utils/open-url";
import { renderTitle } from "./utils/render-title";
@@ -102,8 +102,7 @@ const router = t.router({
const sponsors = await fetchSponsors();
displaySponsors(sponsors);
} catch (error) {
consola.error(error);
process.exit(1);
handleError(error, "Failed to display sponsors");
}
}),
docs: t.procedure

View File

@@ -1,11 +1,11 @@
import { cancel, groupMultiselect, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { groupMultiselect, isCancel } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import { type Addons, AddonsSchema, type Frontend } from "../types";
import {
getCompatibleAddons,
validateAddonCompatibility,
} from "../utils/addon-compatibility";
import { exitCancelled } from "../utils/errors";
type AddonOption = {
value: Addons;
@@ -42,6 +42,10 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
label = "Ultracite";
hint = "Zero-config Biome preset with AI integration";
break;
case "vibe-rules":
label = "vibe-rules";
hint = "Install and apply BTS rules to editors";
break;
case "husky":
label = "Husky";
hint = "Modern native Git hooks made easy";
@@ -65,7 +69,7 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
const ADDON_GROUPS = {
Documentation: ["starlight", "fumadocs"],
Linting: ["biome", "oxlint", "ultracite"],
Other: ["turborepo", "pwa", "tauri", "husky"],
Other: ["vibe-rules", "turborepo", "pwa", "tauri", "husky"],
};
export async function getAddonsChoice(
@@ -119,10 +123,7 @@ export async function getAddonsChoice(
selectableGroups: false,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}
@@ -175,10 +176,7 @@ export async function getAddonsToAdd(
selectableGroups: false,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,7 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import type { API, Backend, Frontend } from "../types";
import { allowedApisForFrontends } from "../utils/compatibility-rules";
import { exitCancelled } from "../utils/errors";
export async function getApiChoice(
Api?: API | undefined,
@@ -43,10 +43,7 @@ export async function getApiChoice(
initialValue: apiOptions[0].value,
});
if (isCancel(apiType)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(apiType)) return exitCancelled("Operation cancelled");
return apiType;
}

View File

@@ -1,7 +1,7 @@
import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { confirm, isCancel } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend } from "../types";
import { exitCancelled } from "../utils/errors";
export async function getAuthChoice(
auth: boolean | undefined,
@@ -21,10 +21,7 @@ export async function getAuthChoice(
initialValue: DEFAULT_CONFIG.auth,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,7 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend } from "../types";
import { exitCancelled } from "../utils/errors";
export async function getBackendFrameworkChoice(
backendFramework?: Backend,
@@ -63,10 +63,7 @@ export async function getBackendFrameworkChoice(
initialValue: DEFAULT_CONFIG.backend,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,5 +1,4 @@
import { cancel, group } from "@clack/prompts";
import pc from "picocolors";
import { group } from "@clack/prompts";
import type {
Addons,
API,
@@ -14,6 +13,7 @@ import type {
Runtime,
WebDeploy,
} from "../types";
import { exitCancelled } from "../utils/errors";
import { getAddonsChoice } from "./addons";
import { getApiChoice } from "./api";
import { getAuthChoice } from "./auth";
@@ -102,10 +102,7 @@ export async function gatherConfig(
install: () => getinstallChoice(flags.install),
},
{
onCancel: () => {
cancel(pc.red("Operation cancelled"));
process.exit(0);
},
onCancel: () => exitCancelled("Operation cancelled"),
},
);

View File

@@ -1,6 +1,6 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import type { Backend, DatabaseSetup, ORM, Runtime } from "../types";
import { exitCancelled } from "../utils/errors";
export async function getDBSetupChoice(
databaseType: string,
@@ -101,10 +101,7 @@ export async function getDBSetupChoice(
initialValue: "none",
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,7 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Database, Runtime } from "../types";
import { exitCancelled } from "../utils/errors";
export async function getDatabaseChoice(
database?: Database,
@@ -55,10 +55,7 @@ export async function getDatabaseChoice(
initialValue: DEFAULT_CONFIG.database,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,11 +1,11 @@
import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, multiselect } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { API, Backend, Database, Examples, Frontend } from "../types";
import {
isExampleAIAllowed,
isExampleTodoAllowed,
} from "../utils/compatibility-rules";
import { exitCancelled } from "../utils/errors";
export async function getExamplesChoice(
examples?: Examples[],
@@ -63,10 +63,7 @@ export async function getExamplesChoice(
),
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,8 +1,8 @@
import { cancel, isCancel, multiselect, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, multiselect, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend } from "../types";
import { isFrontendAllowedWithBackend } from "../utils/compatibility-rules";
import { exitCancelled } from "../utils/errors";
export async function getFrontendChoice(
frontendOptions?: Frontend[],
@@ -28,10 +28,7 @@ export async function getFrontendChoice(
initialValues: ["web"],
});
if (isCancel(frontendTypes)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
const result: Frontend[] = [];
@@ -69,7 +66,7 @@ export async function getFrontendChoice(
},
{
value: "tanstack-start" as const,
label: "TanStack Start (vite)",
label: "TanStack Start",
hint: "SSR, Server Functions, API Routes and more with TanStack Router",
},
];
@@ -84,10 +81,7 @@ export async function getFrontendChoice(
initialValue: DEFAULT_CONFIG.frontend[0],
});
if (isCancel(webFramework)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
result.push(webFramework);
}
@@ -110,10 +104,7 @@ export async function getFrontendChoice(
initialValue: "native-nativewind",
});
if (isCancel(nativeFramework)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
result.push(nativeFramework);
}

View File

@@ -1,6 +1,6 @@
import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { confirm, isCancel } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import { exitCancelled } from "../utils/errors";
export async function getGitChoice(git?: boolean) {
if (git !== undefined) return git;
@@ -10,10 +10,7 @@ export async function getGitChoice(git?: boolean) {
initialValue: DEFAULT_CONFIG.git,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,6 +1,6 @@
import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { confirm, isCancel } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import { exitCancelled } from "../utils/errors";
export async function getinstallChoice(install?: boolean) {
if (install !== undefined) return install;
@@ -10,10 +10,7 @@ export async function getinstallChoice(install?: boolean) {
initialValue: DEFAULT_CONFIG.install,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,7 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Database, ORM, Runtime } from "../types";
import { exitCancelled } from "../utils/errors";
const ormOptions = {
prisma: {
@@ -51,10 +51,7 @@ export async function getORMChoice(
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,6 +1,6 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import type { PackageManager } from "../types";
import { exitCancelled } from "../utils/errors";
import { getUserPkgManager } from "../utils/get-package-manager";
export async function getPackageManagerChoice(
@@ -28,10 +28,7 @@ export async function getPackageManagerChoice(
initialValue: detectedPackageManager,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,10 +1,11 @@
import path from "node:path";
import { cancel, isCancel, text } from "@clack/prompts";
import { isCancel, text } from "@clack/prompts";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import { ProjectNameSchema } from "../types";
import { exitCancelled } from "../utils/errors";
function isPathWithinCwd(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
@@ -76,10 +77,7 @@ export async function getProjectName(initialName?: string): Promise<string> {
},
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled.");
projectPath = response || defaultName;
isValid = true;

View File

@@ -1,7 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Runtime } from "../types";
import { exitCancelled } from "../utils/errors";
export async function getRuntimeChoice(
runtime?: Runtime,
@@ -48,10 +48,7 @@ export async function getRuntimeChoice(
initialValue: DEFAULT_CONFIG.runtime,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -1,8 +1,8 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { isCancel, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend, Runtime, WebDeploy } from "../types";
import { WEB_FRAMEWORKS } from "../utils/compatibility";
import { exitCancelled } from "../utils/errors";
function hasWebFrontend(frontends: Frontend[]): boolean {
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
@@ -56,10 +56,7 @@ export async function getDeploymentChoice(
initialValue: DEFAULT_CONFIG.webDeploy,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}
@@ -105,10 +102,7 @@ export async function getDeploymentToAdd(
initialValue: DEFAULT_CONFIG.webDeploy,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
}

View File

@@ -43,6 +43,7 @@ export const AddonsSchema = z
"starlight",
"biome",
"husky",
"vibe-rules",
"turborepo",
"fumadocs",
"ultracite",

View File

@@ -1,4 +1,3 @@
import { consola } from "consola";
import type {
Addons,
API,
@@ -9,6 +8,7 @@ import type {
} from "../types";
import { validateAddonCompatibility } from "./addon-compatibility";
import { WEB_FRAMEWORKS } from "./compatibility";
import { exitWithError } from "./errors";
export function isWebFrontend(value: Frontend): boolean {
return WEB_FRAMEWORKS.includes(value);
@@ -28,16 +28,14 @@ export function splitFrontends(values: Frontend[] = []): {
export function ensureSingleWebAndNative(frontends: Frontend[]) {
const { web, native } = splitFrontends(frontends);
if (web.length > 1) {
consola.fatal(
exitWithError(
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
);
process.exit(1);
}
if (native.length > 1) {
consola.fatal(
exitWithError(
"Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
);
process.exit(1);
}
}
@@ -52,10 +50,9 @@ export function validateWorkersCompatibility(
config.backend &&
config.backend !== "hono"
) {
consola.fatal(
exitWithError(
`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`,
);
process.exit(1);
}
if (
@@ -64,10 +61,9 @@ export function validateWorkersCompatibility(
config.backend !== "hono" &&
config.runtime === "workers"
) {
consola.fatal(
exitWithError(
`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`,
);
process.exit(1);
}
if (
@@ -77,10 +73,9 @@ export function validateWorkersCompatibility(
config.orm !== "drizzle" &&
config.orm !== "none"
) {
consola.fatal(
exitWithError(
`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
);
process.exit(1);
}
if (
@@ -90,10 +85,9 @@ export function validateWorkersCompatibility(
config.orm !== "none" &&
config.runtime === "workers"
) {
consola.fatal(
exitWithError(
`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
);
process.exit(1);
}
if (
@@ -101,10 +95,9 @@ export function validateWorkersCompatibility(
options.runtime === "workers" &&
config.database === "mongodb"
) {
consola.fatal(
exitWithError(
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
);
process.exit(1);
}
if (
@@ -112,10 +105,9 @@ export function validateWorkersCompatibility(
options.runtime === "workers" &&
config.dbSetup === "docker"
) {
consola.fatal(
exitWithError(
"Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
);
process.exit(1);
}
if (
@@ -123,10 +115,9 @@ export function validateWorkersCompatibility(
config.database === "mongodb" &&
config.runtime === "workers"
) {
consola.fatal(
exitWithError(
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
);
process.exit(1);
}
if (
@@ -134,10 +125,9 @@ export function validateWorkersCompatibility(
options.dbSetup === "docker" &&
config.runtime === "workers"
) {
consola.fatal(
exitWithError(
"Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
);
process.exit(1);
}
}
@@ -209,10 +199,9 @@ export function validateApiFrontendCompatibility(
const includesSvelte = frontends.includes("svelte");
const includesSolid = frontends.includes("solid");
if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") {
consola.fatal(
exitWithError(
`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`,
);
process.exit(1);
}
}
@@ -257,10 +246,9 @@ export function validateWebDeployRequiresWebFrontend(
hasWebFrontendFlag: boolean,
) {
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) {
consola.fatal(
exitWithError(
"'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.",
);
process.exit(1);
}
}
@@ -275,8 +263,7 @@ export function validateAddonsAgainstFrontends(
frontends,
);
if (!isCompatible) {
consola.fatal(`Incompatible addon/frontend combination: ${reason}`);
process.exit(1);
exitWithError(`Incompatible addon/frontend combination: ${reason}`);
}
}
}
@@ -295,21 +282,18 @@ export function validateExamplesCompatibility(
backend !== "none" &&
database === "none"
) {
consola.fatal(
exitWithError(
"The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.",
);
process.exit(1);
}
if (examplesArr.includes("ai") && backend === "elysia") {
consola.fatal(
exitWithError(
"The 'ai' example is not compatible with the Elysia backend.",
);
process.exit(1);
}
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) {
consola.fatal(
exitWithError(
"The 'ai' example is not compatible with the Solid frontend.",
);
process.exit(1);
}
}

View File

@@ -0,0 +1,20 @@
import { cancel } from "@clack/prompts";
import { consola } from "consola";
import pc from "picocolors";
export function exitWithError(message: string): never {
consola.error(pc.red(message));
process.exit(1);
}
export function exitCancelled(message = "Operation cancelled"): never {
cancel(pc.red(message));
process.exit(0);
}
export function handleError(error: unknown, fallbackMessage?: string): never {
const message =
error instanceof Error ? error.message : fallbackMessage || String(error);
consola.error(pc.red(message));
process.exit(1);
}

View File

@@ -1,9 +1,9 @@
import path from "node:path";
import { cancel, isCancel, log, select, spinner } from "@clack/prompts";
import { consola } from "consola";
import { isCancel, log, select, spinner } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import { getProjectName } from "../prompts/project-name";
import { exitCancelled, handleError } from "./errors";
export async function handleDirectoryConflict(
currentPathInput: string,
@@ -49,10 +49,7 @@ export async function handleDirectoryConflict(
initialValue: "rename",
});
if (isCancel(action)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
if (isCancel(action)) return exitCancelled("Operation cancelled.");
switch (action) {
case "overwrite":
@@ -73,8 +70,7 @@ export async function handleDirectoryConflict(
return await handleDirectoryConflict(newPathInput);
}
case "cancel":
cancel(pc.red("Operation cancelled."));
process.exit(0);
return exitCancelled("Operation cancelled.");
}
}
}
@@ -102,8 +98,7 @@ export async function setupProjectDirectory(
s.stop(`Directory "${finalResolvedPath}" cleared.`);
} catch (error) {
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
consola.error(error);
process.exit(1);
handleError(error);
}
} else {
await fs.ensureDir(finalResolvedPath);

View File

@@ -29,6 +29,7 @@ export async function processTemplate(
}
handlebars.registerHelper("eq", (a, b) => a === b);
handlebars.registerHelper("ne", (a, b) => a !== b);
handlebars.registerHelper("and", (a, b) => a && b);
handlebars.registerHelper("or", (a, b) => a || b);

View File

@@ -1,5 +1,4 @@
import path from "node:path";
import { consola } from "consola";
import {
type Addons,
type API,
@@ -27,6 +26,7 @@ import {
validateWebDeployRequiresWebFrontend,
validateWorkersCompatibility,
} from "./utils/compatibility-rules";
import { exitWithError } from "./utils/errors";
export function processAndValidateFlags(
options: CLIInput,
@@ -43,10 +43,9 @@ export function processAndValidateFlags(
!(options.examples.length === 1 && options.examples[0] === "none") &&
options.backend !== "convex"
) {
consola.fatal(
exitWithError(
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
);
process.exit(1);
}
}
}
@@ -62,10 +61,9 @@ export function processAndValidateFlags(
config.backend !== "none"
) {
if (providedFlags.has("runtime") && options.runtime === "none") {
consola.fatal(
exitWithError(
`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`,
);
process.exit(1);
}
}
@@ -101,12 +99,11 @@ export function processAndValidateFlags(
if (projectName) {
const result = ProjectNameSchema.safeParse(path.basename(projectName));
if (!result.success) {
consola.fatal(
exitWithError(
`Invalid project name: ${
result.error.issues[0]?.message || "Invalid project name"
}`,
);
process.exit(1);
}
config.projectName = projectName;
} else if (options.projectDirectory) {
@@ -115,12 +112,11 @@ export function processAndValidateFlags(
);
const result = ProjectNameSchema.safeParse(baseName);
if (!result.success) {
consola.fatal(
exitWithError(
`Invalid project name: ${
result.error.issues[0]?.message || "Invalid project name"
}`,
);
process.exit(1);
}
config.projectName = baseName;
}
@@ -128,8 +124,7 @@ export function processAndValidateFlags(
if (options.frontend && options.frontend.length > 0) {
if (options.frontend.includes("none")) {
if (options.frontend.length > 1) {
consola.fatal(`Cannot combine 'none' with other frontend options.`);
process.exit(1);
exitWithError(`Cannot combine 'none' with other frontend options.`);
}
config.frontend = [];
} else {
@@ -153,8 +148,7 @@ export function processAndValidateFlags(
if (options.addons && options.addons.length > 0) {
if (options.addons.includes("none")) {
if (options.addons.length > 1) {
consola.fatal(`Cannot combine 'none' with other addons.`);
process.exit(1);
exitWithError(`Cannot combine 'none' with other addons.`);
}
config.addons = [];
} else {
@@ -166,8 +160,7 @@ export function processAndValidateFlags(
if (options.examples && options.examples.length > 0) {
if (options.examples.includes("none")) {
if (options.examples.length > 1) {
consola.fatal("Cannot combine 'none' with other examples.");
process.exit(1);
exitWithError("Cannot combine 'none' with other examples.");
}
config.examples = [];
} else {
@@ -187,12 +180,11 @@ export function processAndValidateFlags(
options,
);
if (incompatibleFlags.length > 0) {
consola.fatal(
exitWithError(
`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(
", ",
)}. Please remove them.`,
);
process.exit(1);
}
if (
@@ -204,12 +196,11 @@ export function processAndValidateFlags(
(f) => f === "solid",
);
if (incompatibleFrontends.length > 0) {
consola.fatal(
exitWithError(
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
", ",
)}. Please choose a different frontend or backend.`,
);
process.exit(1);
}
}
@@ -222,10 +213,9 @@ export function processAndValidateFlags(
config.orm === "mongoose" &&
config.database !== "mongodb"
) {
consola.fatal(
exitWithError(
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
);
process.exit(1);
}
if (
@@ -236,10 +226,9 @@ export function processAndValidateFlags(
config.orm !== "mongoose" &&
config.orm !== "prisma"
) {
consola.fatal(
exitWithError(
"MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
);
process.exit(1);
}
if (
@@ -248,10 +237,9 @@ export function processAndValidateFlags(
config.orm === "drizzle" &&
config.database === "mongodb"
) {
consola.fatal(
exitWithError(
"Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
);
process.exit(1);
}
if (
@@ -261,10 +249,9 @@ export function processAndValidateFlags(
config.database !== "none" &&
config.orm === "none"
) {
consola.fatal(
exitWithError(
"Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.",
);
process.exit(1);
}
if (
@@ -274,10 +261,9 @@ export function processAndValidateFlags(
config.orm !== "none" &&
config.database === "none"
) {
consola.fatal(
exitWithError(
"ORM selection requires a database. Please choose a database or set '--orm none'.",
);
process.exit(1);
}
if (
@@ -286,10 +272,9 @@ export function processAndValidateFlags(
config.auth &&
config.database === "none"
) {
consola.fatal(
exitWithError(
"Authentication requires a database. Please choose a database or set '--no-auth'.",
);
process.exit(1);
}
if (
@@ -299,10 +284,9 @@ export function processAndValidateFlags(
config.dbSetup !== "none" &&
config.database === "none"
) {
consola.fatal(
exitWithError(
"Database setup requires a database. Please choose a database or set '--db-setup none'.",
);
process.exit(1);
}
if (
@@ -311,10 +295,9 @@ export function processAndValidateFlags(
config.dbSetup === "turso" &&
config.database !== "sqlite"
) {
consola.fatal(
exitWithError(
"Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
);
process.exit(1);
}
if (
@@ -323,10 +306,9 @@ export function processAndValidateFlags(
config.dbSetup === "neon" &&
config.database !== "postgres"
) {
consola.fatal(
exitWithError(
"Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
);
process.exit(1);
}
if (
@@ -335,10 +317,9 @@ export function processAndValidateFlags(
config.dbSetup === "prisma-postgres" &&
config.database !== "postgres"
) {
consola.fatal(
exitWithError(
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
);
process.exit(1);
}
if (
@@ -347,10 +328,9 @@ export function processAndValidateFlags(
config.dbSetup === "mongodb-atlas" &&
config.database !== "mongodb"
) {
consola.fatal(
exitWithError(
"MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.",
);
process.exit(1);
}
if (
@@ -359,10 +339,9 @@ export function processAndValidateFlags(
config.dbSetup === "supabase" &&
config.database !== "postgres"
) {
consola.fatal(
exitWithError(
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
);
process.exit(1);
}
if (config.dbSetup === "d1") {
@@ -371,10 +350,9 @@ export function processAndValidateFlags(
(providedFlags.has("dbSetup") && !config.database)
) {
if (config.database !== "sqlite") {
consola.fatal(
exitWithError(
"Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
);
process.exit(1);
}
}
@@ -383,10 +361,9 @@ export function processAndValidateFlags(
(providedFlags.has("dbSetup") && !config.runtime)
) {
if (config.runtime !== "workers") {
consola.fatal(
exitWithError(
"Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.",
);
process.exit(1);
}
}
}
@@ -397,10 +374,9 @@ export function processAndValidateFlags(
config.dbSetup === "docker" &&
config.database === "sqlite"
) {
consola.fatal(
exitWithError(
"Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.",
);
process.exit(1);
}
if (
@@ -409,10 +385,9 @@ export function processAndValidateFlags(
config.dbSetup === "docker" &&
config.runtime === "workers"
) {
consola.fatal(
exitWithError(
"Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
);
process.exit(1);
}
validateWorkersCompatibility(providedFlags, options, config);

View File

@@ -0,0 +1,132 @@
# Better-T-Stack Project Rules
This is a {{projectName}} project created with Better-T-Stack CLI.
## Project Structure
This is a monorepo with the following structure:
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
- **`apps/web/`** - Frontend application{{#if (includes frontend "tanstack-router")}} (React with TanStack Router){{else
if (includes frontend "react-router")}} (React with React Router){{else if (includes frontend "next")}} (Next.js){{else
if (includes frontend "nuxt")}} (Nuxt.js){{else if (includes frontend "svelte")}} (SvelteKit){{else if (includes
frontend "solid")}} (SolidStart){{/if}}
{{/if}}
{{#if (ne backend "convex")}}
{{#if (ne backend "none")}}
- **`apps/server/`** - Backend server{{#if (eq backend "hono")}} (Hono){{else if (eq backend "express")}}
(Express){{else if (eq backend "fastify")}} (Fastify){{else if (eq backend "elysia")}} (Elysia){{else if (eq backend
"next")}} (Next.js API){{/if}}
{{/if}}
{{else}}
- **`packages/backend/`** - Convex backend functions
{{/if}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
- **`apps/native/`** - React Native mobile app{{#if (includes frontend "native-nativewind")}} (with NativeWind){{else if
(includes frontend "native-unistyles")}} (with Unistyles){{/if}}
{{/if}}
## Available Scripts
- `{{packageManager}} run dev` - Start all apps in development mode
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
- `{{packageManager}} run dev:web` - Start only the web app
{{/if}}
{{#if (ne backend "none")}}
{{#if (ne backend "convex")}}
- `{{packageManager}} run dev:server` - Start only the server
{{/if}}
{{/if}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
- `{{packageManager}} run dev:native` - Start only the native app
{{/if}}
{{#if (and (ne database "none") (ne orm "none") (ne backend "convex"))}}
## Database Commands
All database operations should be run from the server workspace:
- `{{packageManager}} run db:push` - Push schema changes to database
- `{{packageManager}} run db:studio` - Open database studio
- `{{packageManager}} run db:generate` - Generate {{#if (eq orm "drizzle")}}Drizzle{{else if (eq orm
"prisma")}}Prisma{{else}}{{orm}}{{/if}} files
- `{{packageManager}} run db:migrate` - Run database migrations
{{#if (eq orm "drizzle")}}
Database schema files are located in `apps/server/src/db/schema/`
{{else if (eq orm "prisma")}}
Database schema is located in `apps/server/prisma/schema.prisma`
{{else if (eq orm "mongoose")}}
Database models are located in `apps/server/src/db/models/`
{{/if}}
{{/if}}
{{#if (ne api "none")}}
## API Structure
{{#if (eq api "trpc")}}
- tRPC routers are in `apps/server/src/routers/`
- Client-side tRPC utils are in `apps/web/src/utils/trpc.ts`
{{else if (eq api "orpc")}}
- oRPC endpoints are in `apps/server/src/api/`
- Client-side API utils are in `apps/web/src/utils/api.ts`
{{/if}}
{{/if}}
{{#if auth}}
## Authentication
Authentication is enabled in this project:
{{#if (ne backend "convex")}}
- Server auth logic is in `apps/server/src/lib/auth.ts`
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
- Web app auth client is in `apps/web/src/lib/auth-client.ts`
{{/if}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
- Native app auth client is in `apps/native/src/lib/auth-client.ts`
{{/if}}
{{else}}
{{/if}}
{{/if}}
## Adding More Features
You can add additional addons or deployment options to your project using:
```bash
{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}} create-better-t-stack
add
```
Available addons you can add:
- **Documentation**: Starlight, Fumadocs
- **Linting**: Biome, Oxlint, Ultracite
- **Other**: vibe-rules, Turborepo, PWA, Tauri, Husky
You can also add web deployment configurations like Cloudflare Workers support.
## Project Configuration
This project includes a `bts.jsonc` configuration file that stores your Better-T-Stack settings:
- Contains your selected stack configuration (database, ORM, backend, frontend, etc.)
- Used by the CLI to understand your project structure
- Safe to delete if not needed
- Updated automatically when using the `add` command
## Key Points
- This is a {{#if (includes addons "turborepo")}}Turborepo {{/if}}monorepo using {{packageManager}} workspaces
- Each app has its own `package.json` and dependencies
- Run commands from the root to execute across all workspaces
- Run workspace-specific commands with `{{packageManager}} run command-name`
{{#if (includes addons "turborepo")}}
- Turborepo handles build caching and parallel execution
{{/if}}
- Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
create-better-t-stack add` to add more features later

View File

@@ -48,7 +48,7 @@ const showcaseProjects = [
"https://screenshothis.com?utm_source=better-t-stack&utm_medium=showcase&utm_campaign=referer",
tags: [
"oRPC",
"TanStack Start (vite)",
"TanStack Start",
"Hono",
"pnpm",
"PostgreSQL",

View File

@@ -57,7 +57,7 @@ export const TECH_OPTIONS: Record<
},
{
id: "tanstack-start",
name: "TanStack Start (vite)",
name: "TanStack Start",
description:
"Full-stack React and Solid framework powered by TanStack Router",
icon: `${ICON_BASE_URL}/tanstack.svg`,

View File

@@ -19,7 +19,8 @@
"!**/package.json",
"!**/analytics-minimal.json",
"!**/schema.json",
"!**/_generated/**"
"!**/_generated/**",
"!**/.smoke/**"
]
},
"formatter": {