diff --git a/README.md b/README.md
index 8a490af6..549c1e4d 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ npx create-better-t-stack@latest
- Databases: SQLite, PostgreSQL, MySQL, MongoDB (or none)
- ORMs: Drizzle, Prisma, Mongoose (or none)
- Auth: Better-Auth (optional)
-- Addons: Turborepo, PWA, Tauri, Biome, Husky, Starlight, Fumadocs, Ruler, Ultracite, Oxlint
+- Addons: Turborepo, PWA, Tauri, Biome, Lefthook, Husky, Starlight, Fumadocs, Ruler, Ultracite, Oxlint
- Examples: Todo, AI
- DB Setup: Turso, Neon, Supabase, Prisma PostgreSQL, MongoDB Atlas, Cloudflare D1, Docker
- Web Deploy: Cloudflare Workers
diff --git a/apps/cli/README.md b/apps/cli/README.md
index 2a66686b..f78023b7 100644
--- a/apps/cli/README.md
+++ b/apps/cli/README.md
@@ -41,7 +41,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default
| **Database Setup** | • Turso (SQLite)
• Cloudflare D1 (SQLite)
• Neon (PostgreSQL)
• Supabase (PostgreSQL)
• Prisma Postgres
• MongoDB Atlas
• None (manual setup) |
| **Authentication** | Better-Auth (email/password, with more options coming soon) |
| **Styling** | Tailwind CSS with shadcn/ui components |
-| **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Husky (Git hooks)
• Turborepo (optimized builds) |
+| **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Lefthook, Husky (Git hooks)
• Turborepo (optimized builds) |
| **Examples** | • Todo app
• AI Chat interface (using Vercel AI SDK) |
| **Developer Experience** | • Automatic Git initialization
• Package manager choice (npm, pnpm, bun)
• Automatic dependency installation |
@@ -58,7 +58,7 @@ Options:
--auth Include authentication
--no-auth Exclude authentication
--frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-bare, native-uniwind, native-unistyles, none)
- --addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none)
+ --addons Additional addons (pwa, tauri, starlight, biome, lefthook,husky, turborepo, fumadocs, ultracite, oxlint, none)
--examples Examples to include (todo, ai, none)
--git Initialize git repository
--no-git Skip git initialization
@@ -192,7 +192,7 @@ npx create-better-t-stack my-app --frontend none --backend hono --api trpc --dat
- **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
- **Runtime 'none'**: Only available with Convex backend or when backend is 'none'.
- **Cloudflare Workers runtime**: Only compatible with Hono backend, Drizzle ORM (or no ORM), and SQLite database (with D1 setup). Not compatible with MongoDB.
-- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Husky, Turborepo).
+- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook,Husky, Turborepo).
- **Examples 'none'**: Skips all example implementations (todo, AI chat).
- **SvelteKit, Nuxt, and SolidJS** frontends are only compatible with oRPC API layer
- **PWA support** requires React with TanStack Router, React Router, or SolidJS
diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts
index bf39476d..e945a506 100644
--- a/apps/cli/src/constants.ts
+++ b/apps/cli/src/constants.ts
@@ -85,6 +85,7 @@ export const dependencyVersionMap = {
"@biomejs/biome": "^2.2.0",
oxlint: "^1.32.0",
+ lefthook: "^2.0.11",
husky: "^9.1.7",
"lint-staged": "^16.1.2",
@@ -178,6 +179,7 @@ export const ADDON_COMPATIBILITY = {
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid", "next"],
biome: [],
husky: [],
+ lefthook: [],
turborepo: [],
starlight: [],
ultracite: [],
diff --git a/apps/cli/src/helpers/addons/addons-setup.ts b/apps/cli/src/helpers/addons/addons-setup.ts
index 26eb09fc..a3011bac 100644
--- a/apps/cli/src/helpers/addons/addons-setup.ts
+++ b/apps/cli/src/helpers/addons/addons-setup.ts
@@ -58,23 +58,31 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
}
const hasUltracite = addons.includes("ultracite");
const hasBiome = addons.includes("biome");
- const hasHusky = addons.includes("husky");
+ const gitHook = addons.includes("husky")
+ ? "husky"
+ : addons.includes("lefthook")
+ ? "lefthook"
+ : "";
const hasOxlint = addons.includes("oxlint");
if (hasUltracite) {
- await setupUltracite(config, hasHusky);
+ await setupUltracite(config, gitHook);
} else {
if (hasBiome) {
await setupBiome(projectDir);
}
- if (hasHusky) {
+ if (gitHook) {
let linter: "biome" | "oxlint" | undefined;
if (hasOxlint) {
linter = "oxlint";
} else if (hasBiome) {
linter = "biome";
}
- await setupHusky(projectDir, linter);
+ if (gitHook === "husky") {
+ await setupHusky(projectDir, linter);
+ } else if (gitHook === "lefthook") {
+ await setupLefthook(projectDir);
+ }
}
}
@@ -156,6 +164,25 @@ export async function setupHusky(projectDir: string, linter?: "biome" | "oxlint"
}
}
+export async function setupLefthook(projectDir: string) {
+ await addPackageDependency({
+ devDependencies: ["lefthook"],
+ projectDir,
+ });
+
+ const packageJsonPath = path.join(projectDir, "package.json");
+ if (await fs.pathExists(packageJsonPath)) {
+ const packageJson = await fs.readJson(packageJsonPath);
+
+ packageJson.scripts = {
+ ...packageJson.scripts,
+ prepare: "lefthook install",
+ };
+
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
+ }
+}
+
async function setupPwa(projectDir: string, frontends: Frontend[]) {
const isCompatibleFrontend = frontends.some((f) =>
["react-router", "tanstack-router", "solid"].includes(f),
diff --git a/apps/cli/src/helpers/addons/ultracite-setup.ts b/apps/cli/src/helpers/addons/ultracite-setup.ts
index 0da4d416..4775defa 100644
--- a/apps/cli/src/helpers/addons/ultracite-setup.ts
+++ b/apps/cli/src/helpers/addons/ultracite-setup.ts
@@ -2,7 +2,7 @@ import { autocompleteMultiselect, group, log, multiselect, spinner } from "@clac
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";
@@ -130,7 +130,7 @@ function getFrameworksFromFrontend(frontend: string[]): string[] {
return Array.from(frameworks);
}
-export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
+export async function setupUltracite(config: ProjectConfig, gitHook: string) {
const { packageManager, projectDir, frontend } = config;
try {
@@ -197,8 +197,11 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
ultraciteArgs.push("--hooks", ...hooks);
}
- if (hasHusky) {
- ultraciteArgs.push("--integrations", "husky", "lint-staged");
+ if (gitHook) {
+ ultraciteArgs.push("--integrations", `${gitHook}`);
+ if (gitHook === "husky") {
+ ultraciteArgs.push("lint-staged");
+ }
}
const ultraciteArgsString = ultraciteArgs.join(" ");
@@ -215,13 +218,6 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
shell: true,
});
- if (hasHusky) {
- await addPackageDependency({
- devDependencies: ["husky", "lint-staged"],
- projectDir,
- });
- }
-
s.stop("Ultracite setup successfully!");
} catch (error) {
log.error(pc.red("Failed to set up Ultracite"));
diff --git a/apps/cli/src/helpers/core/create-readme.ts b/apps/cli/src/helpers/core/create-readme.ts
index 5fb53d69..73068a38 100644
--- a/apps/cli/src/helpers/core/create-readme.ts
+++ b/apps/cli/src/helpers/core/create-readme.ts
@@ -459,6 +459,8 @@ function generateFeaturesList(
addonsList.push("- **Tauri** - Build native desktop applications");
} else if (addon === "biome") {
addonsList.push("- **Biome** - Linting and formatting");
+ } else if (addon === "lefthook") {
+ addonsList.push("- **Lefthook** - Fast and powerful Git hooks manager");
} else if (addon === "husky") {
addonsList.push("- **Husky** - Git hooks for code quality");
} else if (addon === "starlight") {
diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts
index 9bedc77c..1cadc607 100644
--- a/apps/cli/src/helpers/core/post-installation.ts
+++ b/apps/cli/src/helpers/core/post-installation.ts
@@ -26,7 +26,8 @@ export async function displayPostInstallInstructions(
const runCmd =
packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
const cdCmd = `cd ${relativePath}`;
- const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
+ const hasGitHooksOrBiome =
+ addons?.includes("husky") || addons?.includes("biome") || addons?.includes("lefthook");
const databaseInstructions =
!isConvex && database !== "none"
@@ -42,7 +43,7 @@ export async function displayPostInstallInstructions(
: "";
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
- const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
+ const lintingInstructions = hasGitHooksOrBiome ? getLintingInstructions(runCmd) : "";
const nativeInstructions =
(frontend?.includes("native-bare") ||
frontend?.includes("native-uniwind") ||
diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts
index aa75b7d1..f35b7b8b 100644
--- a/apps/cli/src/prompts/addons.ts
+++ b/apps/cli/src/prompts/addons.ts
@@ -43,6 +43,10 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
label = "Ruler";
hint = "Centralize your AI rules";
break;
+ case "lefthook":
+ label = "Lefthook";
+ hint = "Fast and powerful Git hooks manager";
+ break;
case "husky":
label = "Husky";
hint = "Modern native Git hooks made easy";
@@ -66,7 +70,7 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
const ADDON_GROUPS = {
Documentation: ["starlight", "fumadocs"],
Linting: ["biome", "oxlint", "ultracite"],
- Other: ["ruler", "turborepo", "pwa", "tauri", "husky"],
+ Other: ["ruler", "turborepo", "pwa", "tauri", "lefthook", "husky"],
};
export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[], auth?: Auth) {
diff --git a/apps/cli/templates/addons/lefthook/lefthook.yml.hbs b/apps/cli/templates/addons/lefthook/lefthook.yml.hbs
new file mode 100644
index 00000000..1a2f654a
--- /dev/null
+++ b/apps/cli/templates/addons/lefthook/lefthook.yml.hbs
@@ -0,0 +1,14 @@
+pre-commit:
+ commands:
+{{#if (includes addons "biome")}}
+ biome-check:
+ run: biome check --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
+ stage_fixed: true
+{{else if (includes addons "oxlint")}}
+ oxlint-fix:
+ run: oxlint --fix {staged_files}
+ stage_fixed: true
+{{else}}
+ info:
+ run: echo "Go to https://lefthook.dev/ for more information on configuring hooks!"
+{{/if}}
\ No newline at end of file
diff --git a/apps/cli/templates/addons/ruler/.ruler/bts.md.hbs b/apps/cli/templates/addons/ruler/.ruler/bts.md.hbs
index 5764e3fe..d25d6e25 100644
--- a/apps/cli/templates/addons/ruler/.ruler/bts.md.hbs
+++ b/apps/cli/templates/addons/ruler/.ruler/bts.md.hbs
@@ -116,7 +116,7 @@ add
Available addons you can add:
- **Documentation**: Starlight, Fumadocs
- **Linting**: Biome, Oxlint, Ultracite
-- **Other**: Ruler, Turborepo, PWA, Tauri, Husky
+- **Other**: Ruler, Turborepo, PWA, Tauri, Lefthook, Husky
You can also add web deployment configurations like Cloudflare Workers support.
diff --git a/apps/cli/test/addons.test.ts b/apps/cli/test/addons.test.ts
index 043e845c..9fb21cd5 100644
--- a/apps/cli/test/addons.test.ts
+++ b/apps/cli/test/addons.test.ts
@@ -4,7 +4,7 @@ import { expectError, expectSuccess, runTRPCTest, type TestConfig } from "./test
describe("Addon Configurations", () => {
describe("Universal Addons (no frontend restrictions)", () => {
- const universalAddons = ["biome", "husky", "turborepo", "oxlint"];
+ const universalAddons = ["biome", "lefthook", "husky", "turborepo", "oxlint"];
for (const addon of universalAddons) {
it(`should work with ${addon} addon on any frontend`, async () => {
diff --git a/apps/web/content/docs/cli/options.mdx b/apps/web/content/docs/cli/options.mdx
index 8cd5a8f1..38914c69 100644
--- a/apps/web/content/docs/cli/options.mdx
+++ b/apps/web/content/docs/cli/options.mdx
@@ -248,6 +248,7 @@ Additional features to include:
- `starlight`: Starlight documentation site
- `fumadocs`: Fumadocs documentation site
- `biome`: Biome linting and formatting
+- `lefthook`: Git hooks with Lefthook
- `husky`: Git hooks with Husky
- `turborepo`: Turborepo monorepo setup
- `ultracite`: Ultracite configuration
diff --git a/apps/web/content/docs/index.mdx b/apps/web/content/docs/index.mdx
index b466ec21..58c09277 100644
--- a/apps/web/content/docs/index.mdx
+++ b/apps/web/content/docs/index.mdx
@@ -310,7 +310,7 @@ See the full list in the [CLI Reference](/docs/cli). Key flags:
- `--orm`: drizzle, prisma, mongoose, none
- `--api`: trpc, orpc, none
- `--auth`: better-auth, clerk, none
-- `--addons`: turborepo, pwa, tauri, biome, husky, starlight, fumadocs, ultracite, oxlint, ruler, none
+- `--addons`: turborepo, pwa, tauri, biome, lefthook,husky, starlight, fumadocs, ultracite, oxlint, ruler, none
- `--examples`: todo, ai, none
## Next Steps
diff --git a/apps/web/content/docs/project-structure.mdx b/apps/web/content/docs/project-structure.mdx
index 7db05de2..3659e306 100644
--- a/apps/web/content/docs/project-structure.mdx
+++ b/apps/web/content/docs/project-structure.mdx
@@ -254,7 +254,7 @@ apps/docs/
"backend": "",
"runtime": "",
"frontend": [""] ,
- "addons": [""] ,
+ "addons": [""] ,
"examples": [""] ,
"auth": <"better-auth"|"clerk"|"none">,
"packageManager": "",
diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts
index e60da064..bca724e5 100644
--- a/apps/web/src/lib/constant.ts
+++ b/apps/web/src/lib/constant.ts
@@ -502,6 +502,14 @@ export const TECH_OPTIONS: Record<
color: "from-green-500 to-green-700",
default: false,
},
+ {
+ id: "lefthook",
+ name: "Lefthook",
+ description: "Fast and powerful Git hooks manager",
+ icon: `${ICON_BASE_URL}/lefthook.svg`,
+ color: "from-red-500 to-red-700",
+ default: false,
+ },
{
id: "husky",
name: "Husky",
diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts
index 97c7dec1..1c30d16d 100644
--- a/apps/web/src/lib/stack-utils.ts
+++ b/apps/web/src/lib/stack-utils.ts
@@ -105,6 +105,7 @@ export function generateStackCommand(stack: StackState) {
"tauri",
"starlight",
"biome",
+ "lefthook",
"husky",
"turborepo",
"ultracite",
diff --git a/packages/types/src/schemas.ts b/packages/types/src/schemas.ts
index 6556da7b..36569da6 100644
--- a/packages/types/src/schemas.ts
+++ b/packages/types/src/schemas.ts
@@ -37,6 +37,7 @@ export const AddonsSchema = z
"tauri",
"starlight",
"biome",
+ "lefthook",
"husky",
"ruler",
"turborepo",