Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
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)
--examples <types...> Examples to include (todo, ai, none)
--git Initialize git repository
--no-git Skip git initialization
Expand Down Expand Up @@ -193,7 +193,7 @@ npx create-better-t-stack --frontend none --backend hono --api trpc --database n
- **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
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const dependencyVersionMap = {
oxlint: "^1.34.0",
oxfmt: "^0.19.0",

lefthook: "^2.0.12",
husky: "^9.1.7",
"lint-staged": "^16.1.2",

Expand Down Expand Up @@ -186,6 +187,7 @@ export const ADDON_COMPATIBILITY = {
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid", "next"],
biome: [],
husky: [],
lefthook: [],
turborepo: [],
starlight: [],
ultracite: [],
Expand Down
48 changes: 44 additions & 4 deletions apps/cli/src/helpers/addons/addons-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,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 hasLefthook = addons.includes("lefthook");
const hasOxlint = addons.includes("oxlint");

if (hasUltracite) {
await setupUltracite(config, hasHusky);
const gitHooks = [];
if (hasHusky) gitHooks.push("husky");
if (hasLefthook) gitHooks.push("lefthook");
await setupUltracite(config, gitHooks);
} else {
if (hasBiome) {
await setupBiome(projectDir);
}
if (hasHusky) {
if (hasHusky || hasLefthook) {
let linter: "biome" | "oxlint" | undefined;
if (hasOxlint) {
linter = "oxlint";
} else if (hasBiome) {
linter = "biome";
}
await setupHusky(projectDir, linter);
if (hasHusky) {
await setupHusky(projectDir, linter);
}
if (hasLefthook) {
await setupLefthook(projectDir);
}
}
}

Expand Down Expand Up @@ -142,9 +151,15 @@ export async function setupHusky(projectDir: string, linter?: "biome" | "oxlint"
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);

// Check if lefthook is also present and update prepare script accordingly
const currentPrepare = packageJson.scripts?.prepare;
const prepareScript = currentPrepare?.includes("lefthook install")
? `${currentPrepare} && husky`
: "husky";

packageJson.scripts = {
...packageJson.scripts,
prepare: "husky",
prepare: prepareScript,
};

if (linter === "oxlint") {
Expand All @@ -165,6 +180,31 @@ 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);

// Check if husky is also present and update prepare script accordingly
const currentPrepare = packageJson.scripts?.prepare;
const prepareScript = currentPrepare?.includes("husky")
? `lefthook install && ${currentPrepare}`
: "lefthook install";

packageJson.scripts = {
...packageJson.scripts,
prepare: prepareScript,
};

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),
Expand Down
17 changes: 6 additions & 11 deletions apps/cli/src/helpers/addons/ultracite-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import pc from "picocolors";

import type { ProjectConfig } from "../../types";

import { addPackageDependency } from "../../utils/add-package-deps";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionArgs } from "../../utils/package-runner";
import { setupBiome } from "./addons-setup";
Expand Down Expand Up @@ -132,7 +131,7 @@ function getFrameworksFromFrontend(frontend: string[]): string[] {
return Array.from(frameworks);
}

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

try {
Expand Down Expand Up @@ -199,8 +198,11 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
ultraciteArgs.push("--hooks", ...hooks);
}

if (hasHusky) {
ultraciteArgs.push("--integrations", "husky", "lint-staged");
if (gitHooks.length > 0) {
ultraciteArgs.push("--integrations", gitHooks.join(" "));
if (gitHooks.includes("husky")) {
ultraciteArgs.push("lint-staged");
}
}
Comment on lines +201 to 206
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ultracite-setup.ts" -type f

Repository: AmanVarshney01/create-better-t-stack

Length of output: 128


🏁 Script executed:

cat -n ./apps/cli/src/helpers/addons/ultracite-setup.ts | head -220 | tail -50

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1793


🏁 Script executed:

cat -n ./apps/cli/src/helpers/addons/ultracite-setup.ts | sed -n '182,210p'

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1126


Use spread operator for --integrations flag to match the pattern used for other Ultracite CLI flags.

All other Ultracite CLI flags (lines 185, 189, 193, 197) use the spread operator to expand array elements as separate arguments. Line 201 should follow the same pattern: change ultraciteArgs.push("--integrations", gitHooks.join(" ")) to ultraciteArgs.push("--integrations", ...gitHooks) for consistency.

🤖 Prompt for AI Agents
In apps/cli/src/helpers/addons/ultracite-setup.ts around lines 200 to 205, the
code pushes the integrations flag as a single string by joining gitHooks,
breaking consistency with other flags; change the push to expand the array using
the spread operator so the flag and each integration are separate args (use
"--integrations", ...gitHooks) and keep the existing conditional that adds
"lint-staged" when "husky" is present.

Copy link
Author

Choose a reason for hiding this comment

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

it didn't work when i tried that, but this worked i tested it

Copy link
Contributor

Choose a reason for hiding this comment

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

@therealsamyak, thank you for testing and clarifying! I appreciate you verifying the implementation. If Ultracite's --integrations flag requires the arguments as a single space-separated string rather than individual arguments, then the current approach using gitHooks.join(" ") is correct. Your testing confirms this works as expected.


✏️ Learnings added
Learnt from: therealsamyak
Repo: AmanVarshney01/create-better-t-stack PR: 711
File: apps/cli/src/helpers/addons/ultracite-setup.ts:200-205
Timestamp: 2025-12-18T19:15:48.478Z
Learning: In apps/cli/src/helpers/addons/ultracite-setup.ts, the Ultracite CLI's `--integrations` flag requires git hooks to be passed as a single space-separated string (e.g., `gitHooks.join(" ")`) rather than using the spread operator like other Ultracite flags. This has been tested and confirmed to work.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: AmanVarshney01/create-better-t-stack PR: 0
File: .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc:0-0
Timestamp: 2025-12-03T07:48:26.419Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `Bun.$` template literal syntax instead of `execa` for shell command execution

Comment on lines +201 to 206
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 24, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for Ultracite CLI documentation or comments
rg -i "ultracite|--integrations" --type ts --type tsx -B 2 -A 2 | head -100

Repository: AmanVarshney01/create-better-t-stack

Length of output: 107


🏁 Script executed:

# Look for test files related to ultracite-setup
fd -e test -e spec | grep -i ultracite

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Search for any README or documentation about Ultracite CLI
fd -e md -e txt | xargs rg -l "ultracite" -i 2>/dev/null

Repository: AmanVarshney01/create-better-t-stack

Length of output: 108


🏁 Script executed:

# Look for ultracite package.json or dependencies
rg -r "ultracite" package.json

Repository: AmanVarshney01/create-better-t-stack

Length of output: 5094


🏁 Script executed:

# Read the ultracite-setup.ts file to see the full context
cat -n apps/cli/src/helpers/addons/ultracite-setup.ts | head -250

Repository: AmanVarshney01/create-better-t-stack

Length of output: 6769


🏁 Script executed:

# Search for other usages of --integrations in the codebase
rg "\-\-integrations" --type ts -B 3 -A 3

Repository: AmanVarshney01/create-better-t-stack

Length of output: 601


🏁 Script executed:

# Look at the CLI README for documentation
cat -n apps/cli/README.md

Repository: AmanVarshney01/create-better-t-stack

Length of output: 12774


🏁 Script executed:

# Search for tests related to ultracite-setup
fd -type f -name "*.test.*" -o -name "*.spec.*" | xargs rg -l "ultracite" 2>/dev/null

Repository: AmanVarshney01/create-better-t-stack

Length of output: 670


🏁 Script executed:

# Search for any comments or documentation about the integrations flag
rg "integrations" --type ts -B 5 -A 5

Repository: AmanVarshney01/create-better-t-stack

Length of output: 871


🏁 Script executed:

# Look for Ultracite documentation or example usage
fd -type f \( -name "*.md" -o -name "*.txt" \) -exec rg -l "integrations" {} \;

Repository: AmanVarshney01/create-better-t-stack

Length of output: 250


🏁 Script executed:

# Check if there's any external documentation link or reference
rg -i "ultracite.*cli\|ultracite.*doc" --type ts -B 2 -A 2

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Check the test file for ultracite usage patterns
cat -n apps/cli/test/addons.test.ts | grep -A 30 -B 5 ultracite

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1469


🏁 Script executed:

# Search in the addons.ts prompts file
cat -n apps/cli/src/prompts/addons.ts | grep -A 10 -B 10 -i "git\|hook"

Repository: AmanVarshney01/create-better-t-stack

Length of output: 4482


🏁 Script executed:

# Look at the actual command construction logic more carefully
rg "ultraciteArgs\|ultraciteInit" -B 3 -A 3

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Search for any Ultracite CLI documentation or references in the web content
cat -n apps/web/content/docs/cli/options.mdx | head -100

Repository: AmanVarshney01/create-better-t-stack

Length of output: 2889


🏁 Script executed:

# Look for any git hook related tests or examples
rg "git.*hook\|husky\|lefthook" apps/cli/test/ -A 5 -B 5

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Check if there's any config or setup files that show ultracite usage
fd -name "ultracite*" -o -name "*ultracite*"

Repository: AmanVarshney01/create-better-t-stack

Length of output: 310


🏁 Script executed:

# Search for any comments about lint-staged or integrations flag
rg "lint-staged\|integrations" apps/cli/src/helpers/addons/ -B 5 -A 5

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Look for any Ultracite package references or documentation links
rg "ultracite" apps/cli/src/ -l

Repository: AmanVarshney01/create-better-t-stack

Length of output: 273


🏁 Script executed:

# Check if there's any commented out code or alternative approaches
rg "integrations|lint-staged" apps/cli/ -B 2 -A 2

Repository: AmanVarshney01/create-better-t-stack

Length of output: 2698


🏁 Script executed:

# Look for how git hooks are configured in the project
rg "husky\|lefthook" apps/cli/src/helpers/addons/ -B 3 -A 3

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Check if there are any examples or configurations showing ultracite init usage
rg "ultracite.*init\|init.*ultracite" apps/cli/ -B 2 -A 2

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Search for any PR or commit messages that might explain the lint-staged logic
git log --oneline --all | head -50

Repository: AmanVarshney01/create-better-t-stack

Length of output: 2527


🏁 Script executed:

# Look for the commit that added ultracite setup
git log --oneline --all | grep -i ultracite

Repository: AmanVarshney01/create-better-t-stack

Length of output: 764


🏁 Script executed:

# Check the PR #711 or related commits for context
git log --oneline --all | head -100 | grep -i "ultracite\|711"

Repository: AmanVarshney01/create-better-t-stack

Length of output: 275


🏁 Script executed:

# Search for any integration related to the actual ultracite command
rg "getPackageExecutionCommand" apps/cli/src/helpers/addons/ultracite-setup.ts -B 10 -A 10

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1229


🏁 Script executed:

# Look at what the final command string looks like
rg "ultraciteArgsString\|commandWithArgs" apps/cli/src/helpers/addons/ultracite-setup.ts -B 5 -A 5

Repository: AmanVarshney01/create-better-t-stack

Length of output: 62


🏁 Script executed:

# Check the specific commit that mentions ultracite setup arguments
git show b2d33a8 --stat

Repository: AmanVarshney01/create-better-t-stack

Length of output: 476


🏁 Script executed:

# Look at the hooks flag fix commit
git show 94e96e2 --stat

Repository: AmanVarshney01/create-better-t-stack

Length of output: 404


🏁 Script executed:

# Let's check if there's any test that shows the expected command format
cat -n apps/cli/test/addons.test.ts | grep -A 50 -B 5 "ultracite"

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1561


🏁 Script executed:

# Check the full diff from commit 94e96e2 that added hooks flag
git show 94e96e2

Repository: AmanVarshney01/create-better-t-stack

Length of output: 2155


🏁 Script executed:

# Also check the earlier commit b2d33a8
git show b2d33a8

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1386


🏁 Script executed:

# Let's trace how the function is called to understand gitHooks parameter
rg "setupUltracite" apps/cli/src/ -B 3 -A 3

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1726


🏁 Script executed:

# Check how gitHooks array is constructed before being passed
rg "gitHooks" apps/cli/src/helpers/ -B 2 -A 2

Repository: AmanVarshney01/create-better-t-stack

Length of output: 1552


lint-staged should be included in the --integrations string, not pushed as a separate argument.

The current code results in --integrations "husky lefthook" lint-staged, making lint-staged a standalone positional argument. According to the git history, the integration should be: --integrations "husky lefthook lint-staged". The proposed fix correctly addresses this by building the integrations array first, adding lint-staged conditionally when husky is present, then joining all values together.

🤖 Prompt for AI Agents
In apps/cli/src/helpers/addons/ultracite-setup.ts around lines 200 to 205, the
code currently pushes "--integrations" with gitHooks.join(" ") then separately
pushes "lint-staged" when "husky" is present, causing lint-staged to become a
standalone positional arg; instead, add "lint-staged" into the integrations
array when husky is detected and then push a single "--integrations" argument
with the joined integrations string (remove the separate push of "lint-staged").

Copy link
Author

Choose a reason for hiding this comment

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

Wrong i tested it and it worked this way

Copy link
Contributor

Choose a reason for hiding this comment

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

@therealsamyak, thank you for confirming! I appreciate you testing this and verifying the behavior. If the Ultracite CLI accepts lint-staged as a separate positional argument after the --integrations flag (resulting in --integrations "husky lefthook" lint-staged), then the current implementation is correct as-is.

I've noted this for future reference to avoid similar confusion.


✏️ Learnings added
Learnt from: therealsamyak
Repo: AmanVarshney01/create-better-t-stack PR: 711
File: apps/cli/src/helpers/addons/ultracite-setup.ts:200-205
Timestamp: 2025-12-25T00:49:53.633Z
Learning: In apps/cli/src/helpers/addons/ultracite-setup.ts, when calling Ultracite CLI with the `--integrations` flag, `lint-staged` should be passed as a separate positional argument after the integrations string (e.g., `ultraciteArgs.push("--integrations", gitHooks.join(" "))` followed by `ultraciteArgs.push("lint-staged")`), not included within the integrations string itself. This has been tested and confirmed to work.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: therealsamyak
Repo: AmanVarshney01/create-better-t-stack PR: 711
File: apps/cli/src/helpers/addons/ultracite-setup.ts:200-205
Timestamp: 2025-12-18T19:15:55.011Z
Learning: In apps/cli/src/helpers/addons/ultracite-setup.ts, the Ultracite CLI's `--integrations` flag requires git hooks to be passed as a single space-separated string (e.g., `gitHooks.join(" ")`) rather than using the spread operator like other Ultracite flags. This has been tested and confirmed to work.


const ultraciteArgsString = ultraciteArgs.join(" ");
Expand All @@ -212,13 +214,6 @@ export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {

await $({ cwd: projectDir, env: { CI: "true" } })`${args}`;

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"));
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 @@ -465,6 +465,8 @@ function generateFeaturesList(
addonsList.push("- **Biome** - Linting and formatting");
} else if (addon === "oxlint") {
addonsList.push("- **Oxlint** - Oxlint + Oxfmt (linting & 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
8 changes: 6 additions & 2 deletions apps/cli/src/helpers/core/post-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export async function displayPostInstallInstructions(
packageManager === "npm" ? "npm run" : packageManager === "pnpm" ? "pnpm run" : "bun run";
const cdCmd = `cd ${relativePath}`;
const hasHusky = addons?.includes("husky");
const hasLinting = addons?.includes("biome") || addons?.includes("oxlint");
const hasGitHooksOrLinting =
addons?.includes("husky") ||
addons?.includes("biome") ||
addons?.includes("lefthook") ||
addons?.includes("oxlint");

const databaseInstructions =
!isConvex && database !== "none"
Expand All @@ -56,7 +60,7 @@ export async function displayPostInstallInstructions(

const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
const huskyInstructions = hasHusky ? getHuskyInstructions(runCmd) : "";
const lintingInstructions = hasLinting ? getLintingInstructions(runCmd) : "";
const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
const nativeInstructions =
(frontend?.includes("native-bare") ||
frontend?.includes("native-uniwind") ||
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/helpers/core/template-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,9 +687,13 @@ export async function setupPaymentsTemplate(projectDir: string, context: Project
export async function setupAddonsTemplate(projectDir: string, context: ProjectConfig) {
if (!context.addons || context.addons.length === 0) return;

const hasUltracite = context.addons.includes("ultracite");

for (const addon of context.addons) {
if (addon === "none") continue;

if ((addon === "lefthook" || addon === "husky") && hasUltracite) continue;

let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
let addonDestDir = projectDir;

Expand Down
22 changes: 13 additions & 9 deletions apps/cli/src/prompts/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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 @@ -56,14 +60,7 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
label = "Fumadocs";
hint = "Build excellent documentation site";
break;
case "opentui":
label = "OpenTUI";
hint = "Build terminal user interfaces";
break;
case "wxt":
label = "WXT";
hint = "Build browser extensions";
break;

default:
label = addon;
hint = `Add ${addon}`;
Expand All @@ -75,7 +72,8 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } {
const ADDON_GROUPS = {
Documentation: ["starlight", "fumadocs"],
Linting: ["biome", "oxlint", "ultracite"],
Other: ["ruler", "pwa", "tauri", "husky", "opentui", "wxt", "turborepo"],
"Git Hooks": ["lefthook", "husky"],
Other: ["ruler", "turborepo", "pwa", "tauri"],
};

export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[], auth?: Auth) {
Expand All @@ -85,6 +83,7 @@ export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[],
const groupedOptions: Record<string, AddonOption[]> = {
Documentation: [],
Linting: [],
"Git Hooks": [],
Other: [],
};

Expand All @@ -101,6 +100,8 @@ export async function getAddonsChoice(addons?: Addons[], frontends?: Frontend[],
groupedOptions.Documentation.push(option);
} else if (ADDON_GROUPS.Linting.includes(addon)) {
groupedOptions.Linting.push(option);
} else if (ADDON_GROUPS["Git Hooks"].includes(addon)) {
groupedOptions["Git Hooks"].push(option);
} else if (ADDON_GROUPS.Other.includes(addon)) {
groupedOptions.Other.push(option);
}
Expand Down Expand Up @@ -146,6 +147,7 @@ export async function getAddonsToAdd(
const groupedOptions: Record<string, AddonOption[]> = {
Documentation: [],
Linting: [],
"Git Hooks": [],
Other: [],
};

Expand All @@ -166,6 +168,8 @@ export async function getAddonsToAdd(
groupedOptions.Documentation.push(option);
} else if (ADDON_GROUPS.Linting.includes(addon)) {
groupedOptions.Linting.push(option);
} else if (ADDON_GROUPS["Git Hooks"].includes(addon)) {
groupedOptions["Git Hooks"].push(option);
} else if (ADDON_GROUPS.Other.includes(addon)) {
groupedOptions.Other.push(option);
}
Expand Down
14 changes: 14 additions & 0 deletions apps/cli/templates/addons/lefthook/lefthook.yml.hbs
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}}
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
23 changes: 22 additions & 1 deletion apps/cli/test/addons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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 Expand Up @@ -195,6 +195,27 @@ describe("Addon Configurations", () => {
expectSuccess(result);
});

it("should work with lefthook and husky together", async () => {
const result = await runTRPCTest({
projectName: "both-git-hooks",
addons: ["lefthook", "husky"],
frontend: ["tanstack-router"],
backend: "hono",
runtime: "bun",
database: "sqlite",
orm: "drizzle",
auth: "none",
api: "trpc",
examples: ["none"],
dbSetup: "none",
webDeploy: "none",
serverDeploy: "none",
install: false,
});

expectSuccess(result);
});

it("should fail with incompatible addon combination", async () => {
const result = await runTRPCTest({
projectName: "incompatible-addons-fail",
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 @@ -291,6 +291,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
3 changes: 2 additions & 1 deletion apps/web/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,9 @@ 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
<<<<<<< HEAD
- `--payments`: polar, none
- `--addons`: turborepo, pwa, tauri, biome, husky, starlight, fumadocs, ultracite, oxlint, ruler, opentui, wxt, none
- `--addons`: turborepo, pwa, tauri, biome, lefthook, husky, starlight, fumadocs, ultracite, oxlint, ruler, opentui, wxt, 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 @@ -259,7 +259,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
Loading