-
-
Notifications
You must be signed in to change notification settings - Fork 234
feat: Add lefthook; refactor out Git hooks to their own section #711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e69fcb2
83d0d66
9d281d5
971ca15
23b3850
40d61ee
9cf5777
a6631ba
0710e58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -41,7 +41,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default | |||||
| | **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) | | ||||||
| | **Authentication** | Better-Auth (email/password, with more options coming soon) | | ||||||
| | **Styling** | Tailwind CSS with shadcn/ui components | | ||||||
| | **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Starlight (documentation site)<br>• Biome (linting and formatting)<br>• Husky (Git hooks)<br>• Turborepo (optimized builds) | | ||||||
| | **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) | | ||||||
| | **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) | | ||||||
| | **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation | | ||||||
|
|
||||||
|
|
@@ -58,7 +58,7 @@ Options: | |||||
| --auth Include authentication | ||||||
| --no-auth Exclude authentication | ||||||
| --frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-bare, native-uniwind, native-unistyles, none) | ||||||
| --addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none) | ||||||
| --addons <types...> Additional addons (pwa, tauri, starlight, biome, lefthook,husky, turborepo, fumadocs, ultracite, oxlint, none) | ||||||
| --examples <types...> 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). | ||||||
|
||||||
| - **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook,Husky, Turborepo). | |
| - **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook, Husky, Turborepo). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
| : ""; | ||
|
Comment on lines
+61
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enforce “only one git hooks manager”: current logic silently prefers Husky. - const gitHook = addons.includes("husky")
- ? "husky"
- : addons.includes("lefthook")
- ? "lefthook"
- : "";
+ const hasHusky = addons.includes("husky");
+ const hasLefthook = addons.includes("lefthook");
+ if (hasHusky && hasLefthook) {
+ throw new Error("Only one git hooks manager can be selected: husky or lefthook.");
+ }
+ const gitHook = hasHusky ? "husky" : hasLefthook ? "lefthook" : null;(You’ll need to adjust the later Also applies to: 69-82 🤖 Prompt for AI Agents
therealsamyak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
|
Comment on lines
+81
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass linter parameter to When Currently, Line 84 calls Apply this diff: if (gitHook === "husky") {
await setupHusky(projectDir, linter);
} else if (gitHook === "lefthook") {
- await setupLefthook(projectDir);
+ await setupLefthook(projectDir, linter);
}
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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 }); | ||
| } | ||
| } | ||
|
Comment on lines
+167
to
+184
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Complete The function currently only installs the lefthook package and sets the prepare script. According to the PR objectives, when Biome or Oxlint are configured, lefthook should be set up with pre-commit hooks using Update the function signature and add lefthook.yml generation: -export async function setupLefthook(projectDir: string) {
+export async function setupLefthook(projectDir: string, linter?: "biome" | "oxlint") {
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 });
}
+
+ // Generate lefthook.yml with pre-commit hooks if linter is configured
+ if (linter) {
+ const lefthookConfigPath = path.join(projectDir, "lefthook.yml");
+ let command = "";
+
+ if (linter === "biome") {
+ command = "biome check --write .";
+ } else if (linter === "oxlint") {
+ command = "oxlint";
+ }
+
+ const lefthookConfig = `pre-commit:
+ commands:
+ lint:
+ glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
+ run: ${command}
+ stage_fixed: true
+`;
+
+ await fs.writeFile(lefthookConfigPath, lefthookConfig, "utf-8");
+ }
}🤖 Prompt for AI Agents |
||
|
|
||
| async function setupPwa(projectDir: string, frontends: Frontend[]) { | ||
| const isCompatibleFrontend = frontends.some((f) => | ||
| ["react-router", "tanstack-router", "solid"].includes(f), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,11 +70,17 @@ 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"], | ||
therealsamyak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[], auth?: Auth) { | ||
| if (addons !== undefined) return addons; | ||
| export async function getAddonsChoice( | ||
| initialAddons?: Addons[], | ||
| frontends?: Frontend[], | ||
| auth?: Auth, | ||
| ) { | ||
| if (initialAddons !== undefined) return initialAddons; | ||
|
|
||
| const existingAddons: Addons[] = []; | ||
|
|
||
| const allAddons = AddonsSchema.options.filter((addon) => addon !== "none"); | ||
| const groupedOptions: Record<string, AddonOption[]> = { | ||
|
|
@@ -85,6 +95,14 @@ export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[], | |
| const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth); | ||
| if (!isCompatible) continue; | ||
|
|
||
| // Skip husky if lefthook is already selected, and vice versa | ||
| if ( | ||
| (addon === "husky" && existingAddons.includes("lefthook")) || | ||
| (addon === "lefthook" && existingAddons.includes("husky")) | ||
| ) { | ||
| continue; | ||
| } | ||
|
Comment on lines
+98
to
+104
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove ineffective skip logic: The Remove the ineffective skip logic: - // Skip husky if lefthook is already selected, and vice versa
- if (
- (addon === "husky" && existingAddons.includes("lefthook")) ||
- (addon === "lefthook" && existingAddons.includes("husky"))
- ) {
- continue;
- }
-Also remove the now-unused if (initialAddons !== undefined) return initialAddons;
- const existingAddons: Addons[] = [];
-
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
🤖 Prompt for AI Agents |
||
|
|
||
| const { label, hint } = getAddonDisplay(addon); | ||
| const option = { value: addon, label, hint }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -902,6 +902,22 @@ export const analyzeStackCompatibility = (stack: StackState): CompatibilityResul | |||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Handle husky/lefthook mutual exclusion | ||||||||||||||||||||||||||||||||||||||||||||||
| const hasHusky = nextStack.addons.includes("husky"); | ||||||||||||||||||||||||||||||||||||||||||||||
| const hasLefthook = nextStack.addons.includes("lefthook"); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (hasHusky && hasLefthook) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Remove lefthook if both are selected (prioritize husky) | ||||||||||||||||||||||||||||||||||||||||||||||
| nextStack.addons = nextStack.addons.filter((addon) => addon !== "lefthook"); | ||||||||||||||||||||||||||||||||||||||||||||||
| notes.addons.notes.push("Lefthook is not compatible with Husky. It will be removed."); | ||||||||||||||||||||||||||||||||||||||||||||||
| notes.addons.hasIssue = true; | ||||||||||||||||||||||||||||||||||||||||||||||
| changes.push({ | ||||||||||||||||||||||||||||||||||||||||||||||
| category: "addons", | ||||||||||||||||||||||||||||||||||||||||||||||
| message: "Lefthook removed (not compatible with Husky)", | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| changed = true; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+905
to
+919
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Husky/lefthook check is unreachable for some backend types. This mutual exclusion block is nested inside the Consider moving this block outside the backend-specific conditionals (e.g., after line 63 or before the final return) to ensure the conflict is always resolved. + // Handle husky/lefthook mutual exclusion (backend-agnostic)
+ const hasHusky = nextStack.addons.includes("husky");
+ const hasLefthook = nextStack.addons.includes("lefthook");
+
+ if (hasHusky && hasLefthook) {
+ nextStack.addons = nextStack.addons.filter((addon) => addon !== "lefthook");
+ notes.addons.notes.push("Lefthook is not compatible with Husky. It will be removed.");
+ notes.addons.hasIssue = true;
+ changes.push({
+ category: "addons",
+ message: "Lefthook removed (not compatible with Husky)",
+ });
+ changed = true;
+ }
+
const isConvex = nextStack.backend === "convex";Then remove the duplicate block from lines 905-919.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const originalAddonsLength = nextStack.addons.length; | ||||||||||||||||||||||||||||||||||||||||||||||
| if (incompatibleAddons.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||
| nextStack.addons = nextStack.addons.filter((addon) => !incompatibleAddons.includes(addon)); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1547,6 +1563,18 @@ export const getDisabledReason = ( | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (category === "addons" && optionId === "husky") { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (finalStack.addons.includes("lefthook")) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "Husky is not compatible with Lefthook. Deselect Lefthook first."; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (category === "addons" && optionId === "lefthook") { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (finalStack.addons.includes("husky")) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "Lefthook is not compatible with Husky. Deselect Husky first."; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1566
to
+1576
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disabled reason check is ineffective due to checking post-adjustment state. These checks use To properly disable the option in the UI, check if (category === "addons" && optionId === "husky") {
- if (finalStack.addons.includes("lefthook")) {
+ if (currentStack.addons.includes("lefthook")) {
return "Husky is not compatible with Lefthook. Deselect Lefthook first.";
}
}
if (category === "addons" && optionId === "lefthook") {
- if (finalStack.addons.includes("husky")) {
+ if (currentStack.addons.includes("husky")) {
return "Lefthook is not compatible with Husky. Deselect Husky first.";
}
}Alternatively, move these checks earlier in the function (before line 1140) alongside the other early-return checks for clearer intent and better efficiency. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (category === "examples" && optionId === "todo") { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (finalStack.database === "none" && finalStack.backend !== "convex") { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "Todo example requires a database. Select a database first (or use Convex backend which includes its own database)."; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix CLI docs formatting:
lefthook,huskyshould belefthook, husky.Line 61 and Line 195 have missing spacing after the comma, which is easy to copy/paste wrong and looks inconsistent.
Also applies to: 195-195
🤖 Prompt for AI Agents