Skip to content

Commit 7b71695

Browse files
feat(cli): Add lefthook addon (#711)
* initial lefthook support * lefthook template * lefthook addon * skip lefthook template if no linter selected * template update * always add lefthook.yml file * lint-staged only necessary w/ husky * file fixing * lefthook and husky work in parallel * git hooks group * spacing fix * reordered addons section to make more sense * fixed ultracite compat * skip lefthook template if ultracite configured * coderabbit fixes * newest lefthook version * 'bun check' fixes * hasLinting * logic fixes * fixed merge conflict * feat: add lefthook to dependencyVersionMap * refactor: update lefthook config to use newer jobs syntax - Changed from 'commands' object syntax to 'jobs' array syntax - Added --write flag to biome check for file fixing - Follows latest lefthook documentation recommendations * fix: remove unnecessary glob from oxfmt job * fix: update ultracite setup to match latest CLI - Fixed --integrations flag to pass values correctly - Added lint-staged to integrations array when husky is selected - Added claude hook option - Added new agents: cursor-cli, mistral-vibe, vercel * fix: init git before install dependencies Prepare scripts (lefthook install, husky) require a git repo to exist. Moving git init before install ensures these hooks can be properly set up. * Revert "fix: init git before install dependencies" This reverts commit 475d87d. * fix: make prepare scripts git-aware to avoid install failures The prepare script now checks if a git repo exists before running. If no .git directory exists, the script silently succeeds, avoiding errors during initial install when git init happens after bun install. * fix: husky uses simple prepare, lefthook uses git check - Husky v9+ handles missing .git directory gracefully - Lefthook needs git check as lefthook install fails without git repo * fix: remove biome.json.hbs from ultracite templates Ultracite creates its own linter config via CLI init command. The biome.json template was incorrectly being copied even when oxlint was selected as the linter. * fix: remove prepare script for lefthook (auto-installs via postinstall) * refactor: move lefthook config to template-generator with package manager - Moved lefthook.yml.hbs to template-generator/templates/addons/lefthook/ - Template uses {{packageManager}} for commands (bun oxlint, bun oxfmt) - Removed programmatic generation from addons-setup.ts - Fixes 'command not found' errors for oxlint/oxfmt * chore: regenerate embedded templates with lefthook * feat: add lefthook install instructions to post-install output * fix --------- Co-authored-by: Aman Varshney <amanvarshney.work@gmail.com>
1 parent 99b0936 commit 7b71695

File tree

19 files changed

+19627
-19547
lines changed

19 files changed

+19627
-19547
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ npx create-better-t-stack@latest
4444
- Databases: SQLite, PostgreSQL, MySQL, MongoDB (or none)
4545
- ORMs: Drizzle, Prisma, Mongoose (or none)
4646
- Auth: Better-Auth (optional)
47-
- Addons: Turborepo, PWA, Tauri, Biome, Husky, Starlight, Fumadocs, Ruler, Ultracite, Oxlint
47+
- Addons: Turborepo, PWA, Tauri, Biome, Lefthook, Husky, Starlight, Fumadocs, Ruler, Ultracite, Oxlint
4848
- Examples: Todo, AI
4949
- DB Setup: Turso, Neon, Supabase, Prisma PostgreSQL, MongoDB Atlas, Cloudflare D1, Docker
5050
- Web Deploy: Cloudflare Workers

apps/cli/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default
4141
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
4242
| **Authentication** | Better-Auth (email/password, with more options coming soon) |
4343
| **Styling** | Tailwind CSS with shadcn/ui components |
44-
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Starlight (documentation site)<br>• Biome (linting and formatting)<br>• Husky (Git hooks)<br>• Turborepo (optimized builds) |
44+
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Starlight (documentation site)<br>• Biome (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• Turborepo (optimized builds) |
4545
| **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
4646
| **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
4747

@@ -58,7 +58,7 @@ Options:
5858
--auth Include authentication
5959
--no-auth Exclude authentication
6060
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-bare, native-uniwind, native-unistyles, none)
61-
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none)
61+
--addons <types...> Additional addons (pwa, tauri, starlight, biome, lefthook, husky, turborepo, fumadocs, ultracite, oxlint, none)
6262
--examples <types...> Examples to include (todo, ai, none)
6363
--git Initialize git repository
6464
--no-git Skip git initialization
@@ -193,7 +193,7 @@ npx create-better-t-stack --frontend none --backend hono --api trpc --database n
193193
- **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
194194
- **Runtime 'none'**: Only available with Convex backend or when backend is 'none'.
195195
- **Cloudflare Workers runtime**: Only compatible with Hono backend, Drizzle ORM (or no ORM), and SQLite database (with D1 setup). Not compatible with MongoDB.
196-
- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Husky, Turborepo).
196+
- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook,Husky, Turborepo).
197197
- **Examples 'none'**: Skips all example implementations (todo, AI chat).
198198
- **SvelteKit, Nuxt, and SolidJS** frontends are only compatible with oRPC API layer
199199
- **PWA support** requires React with TanStack Router, React Router, or SolidJS

apps/cli/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const ADDON_COMPATIBILITY = {
5151
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid", "next"],
5252
biome: [],
5353
husky: [],
54+
lefthook: [],
5455
turborepo: [],
5556
starlight: [],
5657
ultracite: [],

apps/cli/src/helpers/addons/addons-setup.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,15 @@ export async function setupAddons(config: ProjectConfig) {
3838
const hasUltracite = addons.includes("ultracite");
3939
const hasBiome = addons.includes("biome");
4040
const hasHusky = addons.includes("husky");
41+
const hasLefthook = addons.includes("lefthook");
4142
const hasOxlint = addons.includes("oxlint");
4243

43-
if (!hasUltracite) {
44+
if (hasUltracite) {
45+
const gitHooks: string[] = [];
46+
if (hasHusky) gitHooks.push("husky");
47+
if (hasLefthook) gitHooks.push("lefthook");
48+
await setupUltracite(config, gitHooks);
49+
} else {
4450
if (hasBiome) {
4551
await setupBiome(projectDir);
4652
}
@@ -49,14 +55,19 @@ export async function setupAddons(config: ProjectConfig) {
4955
await setupOxlint(projectDir, config.packageManager);
5056
}
5157

52-
if (hasHusky) {
58+
if (hasHusky || hasLefthook) {
5359
let linter: "biome" | "oxlint" | undefined;
5460
if (hasOxlint) {
5561
linter = "oxlint";
5662
} else if (hasBiome) {
5763
linter = "biome";
5864
}
59-
await setupHusky(projectDir, linter);
65+
if (hasHusky) {
66+
await setupHusky(projectDir, linter);
67+
}
68+
if (hasLefthook) {
69+
await setupLefthook(projectDir);
70+
}
6071
}
6172
}
6273

@@ -76,10 +87,6 @@ export async function setupAddons(config: ProjectConfig) {
7687
await setupWxt(config);
7788
}
7889

79-
if (hasUltracite) {
80-
await setupUltracite(config, hasHusky);
81-
}
82-
8390
if (addons.includes("ruler")) {
8491
await setupRuler(config);
8592
}
@@ -136,3 +143,11 @@ export async function setupHusky(projectDir: string, linter?: "biome" | "oxlint"
136143
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
137144
}
138145
}
146+
147+
export async function setupLefthook(projectDir: string) {
148+
await addPackageDependency({
149+
devDependencies: ["lefthook"],
150+
projectDir,
151+
});
152+
// lefthook.yml is generated by template-generator from templates/addons/lefthook/
153+
}

apps/cli/src/helpers/addons/ultracite-setup.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ type UltraciteAgent =
4141
| "crush"
4242
| "qwen"
4343
| "amazon-q-cli"
44-
| "firebender";
44+
| "firebender"
45+
| "cursor-cli"
46+
| "mistral-vibe"
47+
| "vercel";
4548

46-
type UltraciteHook = "cursor" | "windsurf";
49+
type UltraciteHook = "cursor" | "windsurf" | "claude";
4750

4851
const LINTERS = {
4952
biome: { label: "Biome", hint: "Fast formatter and linter" },
@@ -85,11 +88,15 @@ const AGENTS = {
8588
qwen: { label: "Qwen" },
8689
"amazon-q-cli": { label: "Amazon Q CLI" },
8790
firebender: { label: "Firebender" },
91+
"cursor-cli": { label: "Cursor CLI" },
92+
"mistral-vibe": { label: "Mistral Vibe" },
93+
vercel: { label: "Vercel" },
8894
} as const;
8995

9096
const HOOKS = {
9197
cursor: { label: "Cursor" },
9298
windsurf: { label: "Windsurf" },
99+
claude: { label: "Claude" },
93100
} as const;
94101

95102
function getFrameworksFromFrontend(frontend: string[]): string[] {
@@ -117,7 +124,7 @@ function getFrameworksFromFrontend(frontend: string[]): string[] {
117124
return Array.from(frameworks);
118125
}
119126

120-
export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
127+
export async function setupUltracite(config: ProjectConfig, gitHooks: string[]) {
121128
const { packageManager, projectDir, frontend } = config;
122129

123130
try {
@@ -193,8 +200,12 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
193200
ultraciteArgs.push("--hooks", ...hooks);
194201
}
195202

196-
if (hasHusky) {
197-
ultraciteArgs.push("--integrations", "husky", "lint-staged");
203+
if (gitHooks.length > 0) {
204+
const integrations = [...gitHooks];
205+
if (gitHooks.includes("husky")) {
206+
integrations.push("lint-staged");
207+
}
208+
ultraciteArgs.push("--integrations", ...integrations);
198209
}
199210

200211
const ultraciteArgsString = ultraciteArgs.join(" ");

apps/cli/src/helpers/core/post-installation.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export async function displayPostInstallInstructions(
3939
packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
4040
const cdCmd = `cd ${relativePath}`;
4141
const hasHusky = addons?.includes("husky");
42-
const hasLinting = addons?.includes("biome") || addons?.includes("oxlint");
42+
const hasLefthook = addons?.includes("lefthook");
43+
const hasGitHooksOrLinting =
44+
addons?.includes("husky") ||
45+
addons?.includes("biome") ||
46+
addons?.includes("lefthook") ||
47+
addons?.includes("oxlint");
4348

4449
const databaseInstructions =
4550
!isConvex && database !== "none"
@@ -56,7 +61,8 @@ export async function displayPostInstallInstructions(
5661

5762
const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
5863
const huskyInstructions = hasHusky ? getHuskyInstructions(runCmd) : "";
59-
const lintingInstructions = hasLinting ? getLintingInstructions(runCmd) : "";
64+
const lefthookInstructions = hasLefthook ? getLefthookInstructions(packageManager) : "";
65+
const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
6066
const nativeInstructions =
6167
(frontend?.includes("native-bare") ||
6268
frontend?.includes("native-uniwind") ||
@@ -184,6 +190,7 @@ export async function displayPostInstallInstructions(
184190
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
185191
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
186192
if (huskyInstructions) output += `\n${huskyInstructions.trim()}\n`;
193+
if (lefthookInstructions) output += `\n${lefthookInstructions.trim()}\n`;
187194
if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
188195
if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
189196
if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
@@ -250,6 +257,13 @@ function getLintingInstructions(runCmd: string) {
250257
)} Format and lint fix: ${`${runCmd} check`}\n`;
251258
}
252259

260+
function getLefthookInstructions(packageManager: string) {
261+
const cmd = packageManager === "npm" ? "npx" : packageManager;
262+
return `${pc.bold("Git hooks with Lefthook:")}\n${pc.cyan(
263+
"•",
264+
)} Install hooks: ${cmd} lefthook install\n`;
265+
}
266+
253267
async function getDatabaseInstructions(
254268
database: Database,
255269
orm: ORM,

apps/cli/src/prompts/addons.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
4343
label = "Ruler";
4444
hint = "Centralize your AI rules";
4545
break;
46+
case "lefthook":
47+
label = "Lefthook";
48+
hint = "Fast and powerful Git hooks manager";
49+
break;
4650
case "husky":
4751
label = "Husky";
4852
hint = "Modern native Git hooks made easy";
@@ -72,7 +76,7 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
7276
}
7377

7478
const ADDON_GROUPS = {
75-
Tooling: ["turborepo", "biome", "oxlint", "ultracite", "husky"],
79+
Tooling: ["turborepo", "biome", "oxlint", "ultracite", "husky", "lefthook"],
7680
Documentation: ["starlight", "fumadocs"],
7781
Extensions: ["pwa", "tauri", "opentui", "wxt", "ruler"],
7882
};

apps/cli/test/addons.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { expectError, expectSuccess, runTRPCTest, type TestConfig } from "./test
66

77
describe("Addon Configurations", () => {
88
describe("Universal Addons (no frontend restrictions)", () => {
9-
const universalAddons = ["biome", "husky", "turborepo", "oxlint"];
9+
const universalAddons = ["biome", "lefthook", "husky", "turborepo", "oxlint"];
1010

1111
for (const addon of universalAddons) {
1212
it(`should work with ${addon} addon on any frontend`, async () => {
@@ -195,6 +195,27 @@ describe("Addon Configurations", () => {
195195
expectSuccess(result);
196196
});
197197

198+
it("should work with lefthook and husky together", async () => {
199+
const result = await runTRPCTest({
200+
projectName: "both-git-hooks",
201+
addons: ["lefthook", "husky"],
202+
frontend: ["tanstack-router"],
203+
backend: "hono",
204+
runtime: "bun",
205+
database: "sqlite",
206+
orm: "drizzle",
207+
auth: "none",
208+
api: "trpc",
209+
examples: ["none"],
210+
dbSetup: "none",
211+
webDeploy: "none",
212+
serverDeploy: "none",
213+
install: false,
214+
});
215+
216+
expectSuccess(result);
217+
});
218+
198219
it("should fail with incompatible addon combination", async () => {
199220
const result = await runTRPCTest({
200221
projectName: "incompatible-addons-fail",

apps/web/content/docs/cli/options.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ Additional features to include:
291291
- `starlight`: Starlight documentation site
292292
- `fumadocs`: Fumadocs documentation site
293293
- `biome`: Biome linting and formatting
294+
- `lefthook`: Git hooks with Lefthook
294295
- `husky`: Git hooks with Husky
295296
- `turborepo`: Turborepo monorepo setup
296297
- `ultracite`: Ultracite configuration

apps/web/content/docs/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ See the full list in the [CLI Reference](/docs/cli). Key flags:
311311
- `--api`: trpc, orpc, none
312312
- `--auth`: better-auth, clerk, none
313313
- `--payments`: polar, none
314-
- `--addons`: turborepo, pwa, tauri, biome, husky, starlight, fumadocs, ultracite, oxlint, ruler, opentui, wxt, none
314+
- `--addons`: turborepo, pwa, tauri, biome, lefthook, husky, starlight, fumadocs, ultracite, oxlint, ruler, opentui, wxt, none
315315
- `--examples`: todo, ai, none
316316

317317
## Next Steps

0 commit comments

Comments
 (0)