
Linter Formatter Init
- 131 installs
- 31 repo stars
- Updated August 2, 2026
- shipshitdev/library
Initialize ESLint, Prettier, or equivalent linter-formatter stacks with shared configs, ignore files, and npm scripts for consistent code style from day one.
About
linter-formatter-init bootstraps lint and format tooling—configs, ignores, and scripts—so new repos enforce consistent style automatically and agents write code that passes review without manual cleanup passes.
- ESLint and Prettier configuration
- Shared config and ignore files
- npm script wiring
- Editor and CI alignment
- Baseline rule set selection
Linter Formatter Init by the numbers
- 131 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #395 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/shipshitdev/library --skill linter-formatter-initAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 131 |
|---|---|
| repo stars | ★ 31 |
| Last updated | August 2, 2026 |
| Repository | shipshitdev/library ↗ |
What it does
Initialize ESLint, Prettier, or equivalent linter-formatter stacks with shared configs, ignore files, and npm scripts for consistent code style from day one.
Files
Linter Formatter Init
Set up comprehensive linting, formatting, and testing for JavaScript/TypeScript projects using Biome 2.3+ (default), Vitest, and Bun.
IMPORTANT: Always uses Biome 2.3+ (latest) - never older versions.
Purpose
This skill automates the setup of:
- Biome for linting + formatting (default, recommended)
- Vitest for testing with coverage (use
--vitestflag) - ESLint + Prettier (legacy, use
--eslintflag) - Husky + lint-staged for pre-commit hooks
- VS Code/Cursor settings for auto-format on save
- bun scripts for manual linting, formatting, and testing
When to Use
Use this skill when:
- Starting a new JS/TS project
- Adding linting to an existing project without tooling
- Standardizing code quality across a team
- Setting up pre-commit hooks to enforce quality
Quick Start
# Default setup (Biome) - RECOMMENDED
python3 scripts/setup.py \
--root /path/to/project
# Use ESLint + Prettier instead (legacy)
python3 scripts/setup.py \
--root /path/to/project \
--eslint
# ESLint + Prettier with TypeScript
python3 scripts/setup.py \
--root /path/to/project \
--eslint \
--typescript
# Skip pre-commit hooks
python3 scripts/setup.py \
--root /path/to/project \
--no-hooks
# Add Vitest testing with 80% coverage threshold
python3 scripts/setup.py \
--root /path/to/project \
--vitest
# Full setup: Biome + Vitest + Husky
python3 scripts/setup.py \
--root /path/to/project \
--vitest \
--coverage 80What Gets Installed
Dependencies
Biome 2.3+ (default):
- @biomejs/biome@latest (always latest, minimum 2.3+)
Vitest (with --vitest):
- vitest
- @vitest/coverage-v8
ESLint + Prettier (legacy, with --eslint):
- eslint
- prettier
- eslint-config-prettier
- eslint-plugin-prettier
- @typescript-eslint/parser (if --typescript)
- @typescript-eslint/eslint-plugin (if --typescript)
Pre-commit hooks:
- husky
- lint-staged
Configuration Files (Biome - Default)
project/
├── biome.json # Biome config (lint + format)
├── .vscode/
│ └── settings.json # Auto-format on save
├── .husky/
│ └── pre-commit # Pre-commit hook
└── package.json # Updated with scripts + lint-stagedConfiguration Files (ESLint + Prettier - Legacy)
project/
├── .eslintrc.json # ESLint config
├── .prettierrc # Prettier config
├── .prettierignore # Prettier ignore patterns
├── .eslintignore # ESLint ignore patterns
├── .vscode/
│ └── settings.json # Auto-format on save
├── .husky/
│ └── pre-commit # Pre-commit hook
└── package.json # Updated with scripts + lint-stagedBun Scripts Added (Biome)
{
"scripts": {
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"format": "biome format --write .",
"format:check": "biome format .",
"check": "biome check .",
"check:fix": "biome check --write ."
}
}Bun Scripts Added (Vitest)
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}Bun Scripts Added (ESLint + Prettier)
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}Biome Configuration (Default)
Biome is a fast, all-in-one linter and formatter. The default config includes:
{
"$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
"assist": {
"actions": {
"source": { "organizeImports": "on" }
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": { "noForEach": "off" },
"style": { "noNonNullAssertion": "off" },
"suspicious": { "noArrayIndexKey": "off", "noExplicitAny": "warn" }
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
}
}Customization
After setup, customize biome.json to adjust:
- Linting rules
- Formatting preferences
- File ignore patterns
Vitest Configuration (with --vitest)
When you use the --vitest flag, this skill creates a vitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node", // or "jsdom" for frontend
include: ["src/**/*.{test,spec}.{ts,tsx}", "**/*.{test,spec}.{ts,tsx}"],
exclude: ["node_modules", "dist", ".next", "build"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
include: ["src/**/*.ts", "src/**/*.tsx"],
exclude: ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
mockReset: true,
restoreMocks: true,
},
});Coverage Thresholds
Default threshold is 80%. Customize with:
python3 scripts/setup.py \
--root /path/to/project \
--vitest \
--coverage 90 # Set to 90%Test Setup File
Creates src/test/setup.ts for global test configuration:
import { expect, afterEach } from "vitest";
import { cleanup } from "@testing-library/react"; // For React projects
// Cleanup after each test
afterEach(() => {
cleanup();
});Pre-commit Hooks
When enabled (default), lint-staged runs on every commit:
Biome (default):
{
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css}": ["bunx biome check --write"]
}
}ESLint + Prettier (legacy):
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": ["prettier --write"]
}
}This ensures:
- All committed code passes linting
- All committed code is formatted
- No broken code enters the repo
VS Code / Cursor Integration
The skill creates .vscode/settings.json:
Biome (default):
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"quickfix.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}ESLint + Prettier (legacy):
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}Why Biome Over ESLint + Prettier?
- Faster: Written in Rust, 10-100x faster than ESLint + Prettier
- Simpler: One tool instead of two, one config file
- No conflicts: No need for eslint-config-prettier or similar workarounds
- Better defaults: Sensible rules out of the box
Monorepo Support
For monorepos, run from the root:
python3 scripts/setup.py \
--root /path/to/monorepo \
--monorepoThis adds root-level config that applies to all packages.
Troubleshooting
Pre-commit hooks not running
# Reinstall husky
bunx husky
chmod +x .husky/pre-commitFormat on save not working (Biome)
1. Install the Biome extension in VS Code/Cursor 2. Set Biome as default formatter 3. Enable "Format on Save" in settings
Format on save not working (ESLint + Prettier)
1. Install the Prettier extension in VS Code/Cursor 2. Set Prettier as default formatter 3. Enable "Format on Save" in settings
Framework-Specific Configs (ESLint mode only)
When using --eslint, the skill detects common frameworks and adjusts config:
- Next.js: Adds
next/core-web-vitalsto ESLint - React: Adds
eslint-plugin-reactandeslint-plugin-react-hooks - NestJS: Adds rules for decorators and DI patterns
Manual Setup (Alternative)
If you prefer manual setup over the script:
Biome:
bun add -D @biomejs/biome husky lint-staged
bunx biome init
bunx huskyESLint + Prettier:
bun add -D eslint prettier eslint-config-prettier eslint-plugin-prettier husky lint-staged
bun add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
bunx eslint --init
bunx huskyThen copy configs from assets/configs/
{
"$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noForEach": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
}
}
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"prefer-const": "error",
"no-var": "error"
}
}
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"prefer-const": "error",
"no-var": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"]
}
}
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
{
"name": "linter-formatter-init",
"version": "1.0.0",
"description": "Set up Biome (default) or ESLint + Prettier, Vitest testing, and pre-commit hooks for any JavaScript",
"author": {
"name": "Ship Shit Dev",
"email": "hello@shipshit.dev",
"url": "https://shipshit.dev"
},
"license": "MIT",
"skills": "."
}
#!/usr/bin/env python3
"""
Linter Formatter Setup - Initialize Biome (default) or ESLint + Prettier, and pre-commit hooks
Usage:
setup.py --root <path> [options]
Options:
--root Project root directory (required)
--typescript Enable TypeScript support (only for ESLint mode)
--eslint Use ESLint + Prettier instead of Biome (legacy)
--no-hooks Skip pre-commit hook setup
--monorepo Configure for monorepo root
--dry-run Show what would be done without making changes
Examples:
setup.py --root /path/to/project # Biome (default)
setup.py --root /path/to/project --eslint # ESLint + Prettier
setup.py --root /path/to/project --eslint --typescript
setup.py --root /path/to/project --no-hooks
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
# ============================================================================
# Configuration Templates
# ============================================================================
BIOME_CONFIG = {
"$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": True,
"rules": {
"recommended": True,
"complexity": {
"noForEach": "off"
},
"style": {
"noNonNullAssertion": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noExplicitAny": "warn"
}
}
},
"formatter": {
"enabled": True,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
},
"files": {
"ignore": [
"node_modules",
"dist",
"build",
".next",
"out",
".cache",
".turbo",
"coverage",
"*.min.js",
"*.min.css"
]
}
}
ESLINT_CONFIG = {
"env": {
"browser": True,
"es2021": True,
"node": True
},
"extends": [
"eslint:recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-console": ["warn", {"allow": ["warn", "error"]}],
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"prefer-const": "error",
"no-var": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"]
}
}
ESLINT_CONFIG_TS = {
"env": {
"browser": True,
"es2021": True,
"node": True
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"no-console": ["warn", {"allow": ["warn", "error"]}],
"@typescript-eslint/no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"prefer-const": "error",
"no-var": "error",
"eqeqeq": ["error", "always"],
"curly": ["error", "all"]
}
}
PRETTIER_CONFIG = {
"semi": True,
"singleQuote": True,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": True,
"arrowParens": "always",
"endOfLine": "lf"
}
PRETTIER_IGNORE = """# Dependencies
node_modules/
# Build outputs
dist/
build/
.next/
out/
# Cache
.cache/
.turbo/
# Coverage
coverage/
# Lock files
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lockb
# Generated
*.min.js
*.min.css
"""
ESLINT_IGNORE = """# Dependencies
node_modules/
# Build outputs
dist/
build/
.next/
out/
# Cache
.cache/
.turbo/
# Coverage
coverage/
"""
VSCODE_SETTINGS_BIOME = {
"editor.formatOnSave": True,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"quickfix.biome": "explicit"
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
VSCODE_SETTINGS_ESLINT = {
"editor.formatOnSave": True,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
LINT_STAGED_CONFIG_BIOME = {
"*.{js,jsx,ts,tsx,json,css}": ["bunx biome check --write"]
}
LINT_STAGED_CONFIG_ESLINT = {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml,css,scss}": [
"prettier --write"
]
}
# ============================================================================
# Helper Functions
# ============================================================================
def detect_package_manager(root: Path) -> str:
"""Detect which package manager is used. ALWAYS prefers bun."""
# Check for bun first - it's the preferred package manager
try:
result = subprocess.run(["which", "bun"], capture_output=True, text=True)
if result.returncode == 0:
return "bun"
except:
pass
# Fall back to checking lock files
if (root / "bun.lockb").exists():
return "bun"
elif (root / "pnpm-lock.yaml").exists():
return "pnpm"
elif (root / "yarn.lock").exists():
return "yarn"
return "npm"
def detect_framework(root: Path) -> list:
"""Detect frameworks used in the project."""
frameworks = []
pkg_json = root / "package.json"
if pkg_json.exists():
try:
pkg = json.loads(pkg_json.read_text())
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
if "next" in deps:
frameworks.append("nextjs")
if "react" in deps:
frameworks.append("react")
if "@nestjs/core" in deps:
frameworks.append("nestjs")
if "express" in deps:
frameworks.append("express")
if "vue" in deps:
frameworks.append("vue")
except:
pass
return frameworks
def run_command(cmd: list, cwd: Path, dry_run: bool = False) -> bool:
"""Run a shell command."""
cmd_str = " ".join(cmd)
if dry_run:
print(f" [DRY-RUN] Would run: {cmd_str}")
return True
try:
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
print(f" Warning: Command had issues: {cmd_str}")
if result.stderr:
print(f" {result.stderr[:200]}")
return False
return True
except Exception as e:
print(f" Error running command: {e}")
return False
def write_json(path: Path, data: dict, dry_run: bool = False):
"""Write JSON to file."""
if dry_run:
print(f" [DRY-RUN] Would write: {path}")
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
print(f" Created: {path.name}")
def write_text(path: Path, content: str, dry_run: bool = False):
"""Write text to file."""
if dry_run:
print(f" [DRY-RUN] Would write: {path}")
return
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
print(f" Created: {path.name}")
def update_package_json(root: Path, scripts: dict, lint_staged: dict, dry_run: bool = False):
"""Update package.json with scripts and lint-staged config."""
pkg_path = root / "package.json"
if not pkg_path.exists():
print(" No package.json found, skipping script additions")
return
if dry_run:
print(f" [DRY-RUN] Would update: package.json")
return
try:
pkg = json.loads(pkg_path.read_text())
# Add scripts
if "scripts" not in pkg:
pkg["scripts"] = {}
pkg["scripts"].update(scripts)
# Add lint-staged
pkg["lint-staged"] = lint_staged
pkg_path.write_text(json.dumps(pkg, indent=2) + "\n")
print(" Updated: package.json")
except Exception as e:
print(f" Error updating package.json: {e}")
# ============================================================================
# Setup Functions
# ============================================================================
def setup_biome(root: Path, dry_run: bool):
"""Set up Biome (default linter/formatter)."""
print("\n Installing Biome...")
pm = detect_package_manager(root)
install_cmd = {
"bun": ["bun", "add", "-D"],
"pnpm": ["pnpm", "add", "-D"],
"yarn": ["yarn", "add", "-D"],
"npm": ["npm", "install", "-D"]
}[pm]
# Always install Biome 2.3+ (latest)
run_command(install_cmd + ["@biomejs/biome@latest"], root, dry_run)
print("\n Creating configuration files...")
write_json(root / "biome.json", BIOME_CONFIG, dry_run)
scripts = {
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"format": "biome format --write .",
"format:check": "biome format .",
"check": "biome check .",
"check:fix": "biome check --write ."
}
update_package_json(root, scripts, LINT_STAGED_CONFIG_BIOME, dry_run)
def setup_eslint_prettier(root: Path, typescript: bool, dry_run: bool):
"""Set up ESLint and Prettier (legacy mode)."""
print("\n Installing ESLint + Prettier dependencies...")
pm = detect_package_manager(root)
install_cmd = {
"bun": ["bun", "add", "-D"],
"pnpm": ["pnpm", "add", "-D"],
"yarn": ["yarn", "add", "-D"],
"npm": ["npm", "install", "-D"]
}[pm]
# Base dependencies
deps = [
"eslint",
"prettier",
"eslint-config-prettier",
"eslint-plugin-prettier"
]
# TypeScript dependencies
if typescript:
deps.extend([
"@typescript-eslint/parser",
"@typescript-eslint/eslint-plugin"
])
# Detect frameworks and add plugins
frameworks = detect_framework(root)
if "react" in frameworks or "nextjs" in frameworks:
deps.extend(["eslint-plugin-react", "eslint-plugin-react-hooks"])
if "nextjs" in frameworks:
deps.append("@next/eslint-plugin-next")
run_command(install_cmd + deps, root, dry_run)
print("\n Creating configuration files...")
# ESLint config
eslint_config = ESLINT_CONFIG_TS if typescript else ESLINT_CONFIG
# Add React config if detected
if "react" in frameworks or "nextjs" in frameworks:
eslint_config["extends"].insert(1, "plugin:react/recommended")
eslint_config["extends"].insert(2, "plugin:react-hooks/recommended")
eslint_config["plugins"] = eslint_config.get("plugins", []) + ["react", "react-hooks"]
eslint_config["settings"] = {"react": {"version": "detect"}}
eslint_config["rules"]["react/react-in-jsx-scope"] = "off"
eslint_config["rules"]["react/prop-types"] = "off"
if "nextjs" in frameworks:
eslint_config["extends"].insert(1, "next/core-web-vitals")
write_json(root / ".eslintrc.json", eslint_config, dry_run)
write_json(root / ".prettierrc", PRETTIER_CONFIG, dry_run)
write_text(root / ".prettierignore", PRETTIER_IGNORE, dry_run)
write_text(root / ".eslintignore", ESLINT_IGNORE, dry_run)
# npm scripts
ext = ".js,.jsx,.ts,.tsx" if typescript else ".js,.jsx"
scripts = {
"lint": f"eslint . --ext {ext}",
"lint:fix": f"eslint . --ext {ext} --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
update_package_json(root, scripts, LINT_STAGED_CONFIG_ESLINT, dry_run)
def setup_husky(root: Path, dry_run: bool):
"""Set up Husky pre-commit hooks."""
print("\n Setting up pre-commit hooks...")
pm = detect_package_manager(root)
install_cmd = {
"bun": ["bun", "add", "-D"],
"pnpm": ["pnpm", "add", "-D"],
"yarn": ["yarn", "add", "-D"],
"npm": ["npm", "install", "-D"]
}[pm]
run_command(install_cmd + ["husky", "lint-staged"], root, dry_run)
# Initialize husky (v9+ uses just "husky" not "husky install")
exec_cmd = "bunx" if pm == "bun" else "npx"
run_command([exec_cmd, "husky"], root, dry_run)
# Create pre-commit hook with appropriate executor
husky_dir = root / ".husky"
if not dry_run:
husky_dir.mkdir(exist_ok=True)
pre_commit_path = husky_dir / "pre-commit"
pre_commit_content = f"""#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
{exec_cmd} lint-staged
"""
write_text(pre_commit_path, pre_commit_content, dry_run)
if not dry_run:
pre_commit_path.chmod(0o755)
# Add prepare script to package.json (v9+ uses just "husky")
pkg_path = root / "package.json"
if pkg_path.exists() and not dry_run:
pkg = json.loads(pkg_path.read_text())
if "scripts" not in pkg:
pkg["scripts"] = {}
pkg["scripts"]["prepare"] = "husky"
pkg_path.write_text(json.dumps(pkg, indent=2) + "\n")
def setup_vscode(root: Path, use_eslint: bool, dry_run: bool):
"""Set up VS Code / Cursor settings."""
print("\n Setting up editor configuration...")
vscode_dir = root / ".vscode"
settings_path = vscode_dir / "settings.json"
settings = VSCODE_SETTINGS_ESLINT.copy() if use_eslint else VSCODE_SETTINGS_BIOME.copy()
# Merge with existing settings if present
if settings_path.exists() and not dry_run:
try:
existing = json.loads(settings_path.read_text())
existing.update(settings)
settings = existing
except:
pass
write_json(settings_path, settings, dry_run)
# ============================================================================
# Main
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description="Set up linting and formatting for JS/TS projects (Biome by default)"
)
parser.add_argument("--root", required=True, help="Project root directory")
parser.add_argument("--typescript", action="store_true", help="Enable TypeScript support (ESLint mode only)")
parser.add_argument("--eslint", action="store_true", help="Use ESLint + Prettier instead of Biome (legacy)")
parser.add_argument("--no-hooks", action="store_true", help="Skip pre-commit hook setup")
parser.add_argument("--monorepo", action="store_true", help="Configure for monorepo root")
parser.add_argument("--dry-run", action="store_true", help="Show what would be done")
# Keep --biome for backwards compatibility (now a no-op since it's the default)
parser.add_argument("--biome", action="store_true", help=argparse.SUPPRESS)
args = parser.parse_args()
root = Path(args.root).resolve()
if not root.exists():
print(f"Error: Directory does not exist: {root}")
sys.exit(1)
if not (root / "package.json").exists():
print(f"Error: No package.json found in {root}")
print(" Run 'bun init' first to create a package.json")
sys.exit(1)
use_eslint = args.eslint
pm = detect_package_manager(root)
print(f"Setting up linting and formatting")
print(f" Project: {root}")
print(f" Package manager: {pm}")
print(f" Tool: {'ESLint + Prettier (legacy)' if use_eslint else 'Biome'}")
if use_eslint:
print(f" TypeScript: {'Yes' if args.typescript else 'No'}")
print(f" Pre-commit hooks: {'No' if args.no_hooks else 'Yes'}")
if args.dry_run:
print("\n DRY RUN MODE - No changes will be made\n")
# Detect frameworks
frameworks = detect_framework(root)
if frameworks:
print(f" Detected: {', '.join(frameworks)}")
# Run setup
if use_eslint:
setup_eslint_prettier(root, args.typescript, args.dry_run)
else:
setup_biome(root, args.dry_run)
if not args.no_hooks:
setup_husky(root, args.dry_run)
setup_vscode(root, use_eslint, args.dry_run)
print("\n Setup complete!")
print("\nNext steps:")
print(f" 1. Review the generated config files")
print(f" 2. Run 'bun run lint' to check for issues")
print(f" 3. Run 'bun run format' to format all files")
if not args.no_hooks:
print(" 4. Make a commit to test the pre-commit hook")
if use_eslint:
print("\nRecommended VS Code extensions:")
print(" - ESLint (dbaeumer.vscode-eslint)")
print(" - Prettier (esbenp.prettier-vscode)")
else:
print("\nRecommended VS Code extension:")
print(" - Biome (biomejs.biome)")
if __name__ == "__main__":
main()