Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix CLI docs formatting: lefthook,husky should be lefthook, husky.
Line 61 and Line 195 have missing spacing after the comma, which is easy to copy/paste wrong and looks inconsistent.

-  --addons <types...>             Additional addons (pwa, tauri, starlight, biome, lefthook,husky, turborepo, fumadocs, ultracite, oxlint, none)
+  --addons <types...>             Additional addons (pwa, tauri, starlight, biome, lefthook, husky, turborepo, fumadocs, ultracite, oxlint, none)

-- - **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook,Husky, Turborepo).
+- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook, Husky, Turborepo).

Also applies to: 195-195

🤖 Prompt for AI Agents
In apps/cli/README.md around lines 61 and 195, the addon list contains
"lefthook,husky" without a space after the comma; update both occurrences to
"lefthook, husky" to ensure consistent formatting and correct copy/paste
behavior.

--examples <types...> Examples to include (todo, ai, none)
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's inconsistent spacing in the option description. The word "Git" at the start of "Git hooks manager" has an extra space before it, making the alignment inconsistent with other options. Should be aligned with a single space like other options.

Copilot uses AI. Check for mistakes.
--git Initialize git repository
--no-git Skip git initialization
Expand Down Expand Up @@ -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).
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing in addons list. There's a missing space in "Lefthook,Husky" but not in "Lefthook, Husky" on line 44. These should both be consistently formatted as "Lefthook, Husky".

Suggested change
- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook,Husky, Turborepo).
- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Lefthook, Husky, Turborepo).

Copilot uses AI. Check for mistakes.
- **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
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
22 changes: 15 additions & 7 deletions apps/cli/src/helpers/addons/addons-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,27 @@ ${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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce “only one git hooks manager”: current logic silently prefers Husky.
At Line 61–65, if a user selects both husky and lefthook, gitHook becomes "husky" without warning. If “only one may be selected” is a real requirement, this should be validated explicitly (ideally where addon compatibility is validated).

-  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 if (gitHook) checks accordingly.)

Also applies to: 69-82

🤖 Prompt for AI Agents
In apps/cli/src/helpers/addons/addons-setup.ts around lines 61-65 (also
affecting logic at 69-82), the current selection logic silently prefers "husky"
when both "husky" and "lefthook" are present; enforce validation so only one
git-hook manager can be selected: detect if both are in the addons array and
throw or return a clear validation error, otherwise set gitHook to the chosen
value based on presence; update subsequent if (gitHook) checks to assume a
single valid value (or handle the error case) so the flow never silently prefers
one over the other.

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);
await setupGitHooks(gitHook, projectDir, linter);
}
}

Expand Down Expand Up @@ -123,9 +127,13 @@ export async function setupBiome(projectDir: string) {
}
}

export async function setupHusky(projectDir: string, linter?: "biome" | "oxlint") {
export async function setupGitHooks(
gitHook: string,
projectDir: string,
linter?: "biome" | "oxlint",
) {
await addPackageDependency({
devDependencies: ["husky", "lint-staged"],
devDependencies: [`${gitHook === "husky" ? "husky" : "lefthook"}`, "lint-staged"],
projectDir,
});

Expand All @@ -135,7 +143,7 @@ export async function setupHusky(projectDir: string, linter?: "biome" | "oxlint"

packageJson.scripts = {
...packageJson.scripts,
prepare: "husky",
prepare: gitHook === "husky" ? "husky" : "lefthook install",
};

if (linter === "oxlint") {
Expand Down
13 changes: 9 additions & 4 deletions apps/cli/src/helpers/addons/ultracite-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -197,8 +197,8 @@ 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}`, "lint-staged");
}

const ultraciteArgsString = ultraciteArgs.join(" ");
Expand All @@ -215,11 +215,16 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
shell: true,
});

if (hasHusky) {
if (gitHook === "husky") {
await addPackageDependency({
devDependencies: ["husky", "lint-staged"],
projectDir,
});
} else if (gitHook === "lefthook") {
await addPackageDependency({
devDependencies: ["lefthook", "lint-staged"],
projectDir,
});
}

s.stop("Ultracite setup successfully!");
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/helpers/core/create-readme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
5 changes: 3 additions & 2 deletions apps/cli/src/helpers/core/post-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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") ||
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/src/prompts/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/templates/addons/ruler/.ruler/bts.md.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/test/addons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions apps/web/content/docs/cli/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/docs/project-structure.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ apps/docs/
"backend": "<none|hono|express|fastify|elysia|next|convex>",
"runtime": "<bun|node|workers>",
"frontend": ["<next|tanstack-router|react-router|tanstack-start|nuxt|svelte|solid>"] ,
"addons": ["<turborepo|biome|husky|pwa|starlight>"] ,
"addons": ["<turborepo|biome|lefthook|husky|pwa|starlight>"] ,
"examples": ["<ai|todo|none>"] ,
"auth": <"better-auth"|"clerk"|"none">,
"packageManager": "<bun|pnpm|npm>",
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/lib/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/stack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function generateStackCommand(stack: StackState) {
"tauri",
"starlight",
"biome",
"lefthook",
"husky",
"turborepo",
"ultracite",
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const AddonsSchema = z
"tauri",
"starlight",
"biome",
"lefthook",
"husky",
"ruler",
"turborepo",
Expand Down