
Husky Test Coverage
- 142 installs
- 31 repo stars
- Updated August 2, 2026
- shipshitdev/library
Bootstrap Husky git hooks with test and coverage gates so commits and pushes run lint, unit tests, and minimum coverage thresholds automatically.
About
husky-test-coverage sets up Husky git hooks that run tests and enforce coverage on commit or push, aligning local developer workflow with CI quality bars and reducing broken merges from untested changes.
- Husky hook installation
- Pre-commit lint and test scripts
- Coverage threshold configuration
- package.json script integration
- CI-aligned local developer workflow
Husky Test Coverage by the numbers
- 142 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #899 of 2,153 Testing & QA 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 husky-test-coverageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 142 |
|---|---|
| repo stars | ★ 31 |
| Last updated | August 2, 2026 |
| Repository | shipshitdev/library ↗ |
What it does
Bootstrap Husky git hooks with test and coverage gates so commits and pushes run lint, unit tests, and minimum coverage thresholds automatically.
Files
Husky Test Coverage
Set up or verify Husky git hooks to ensure tests run and coverage thresholds are enforced on every commit.
Purpose
This skill automates the setup of:
- Husky git hooks for pre-commit testing
- Test runner detection (Jest, Vitest, Mocha)
- Coverage configuration with thresholds (default: 80%)
- Pre-commit hook that runs tests with coverage
- Configurable coverage enforcement (block or warn)
When to Use
This skill should be used when:
- Setting up test coverage enforcement for the first time
- Verifying existing Husky/test setup is correctly configured
- Ensuring coverage thresholds are met before commits
- Configuring pre-commit hooks for test coverage
- Adapting coverage setup to different test runners
Project Context Discovery
Before setting up test coverage, discover the project's context:
1. Check package.json:
- Review existing test scripts
- Detect test runner from dependencies (jest, vitest, mocha)
- Check for existing Husky installation
- Review existing coverage configuration
2. Identify Test Runner:
- Jest: Check for
jestin dependencies, look forjest.config.jsorjest.config.json - Vitest: Check for
vitestin dependencies, look forvitest.config.tsorvitest.config.js - Mocha: Check for
mochain dependencies, check for coverage tool (nyc, c8)
3. Check Coverage Configuration:
- Jest: Look for
coverageThresholdin jest.config.* - Vitest: Look for
coverage.thresholdsin vitest.config.* - Mocha: Look for
.nycrc.jsonor coverage config in package.json
4. Verify Existing Husky Setup:
- Check if
.husky/directory exists - Review existing pre-commit hook
- Check if Husky is in package.json dependencies
5. Detect Test Files:
- Scan for
*.test.*or*.spec.*files - Verify tests exist before enforcing coverage
Quick Start
# Basic setup (80% coverage threshold, blocks commits below threshold)
python3 ${CLAUDE_SKILL_DIR}/scripts/setup-husky-coverage.py \
--root /path/to/project
# Custom threshold (85%)
python3 ${CLAUDE_SKILL_DIR}/scripts/setup-husky-coverage.py \
--root /path/to/project \
--threshold 85
# Warn only (don't block commits)
python3 ${CLAUDE_SKILL_DIR}/scripts/setup-husky-coverage.py \
--root /path/to/project \
--no-fail-on-below
# Skip if no tests found
python3 ${CLAUDE_SKILL_DIR}/scripts/setup-husky-coverage.py \
--root /path/to/project \
--skip-if-no-tests
# Dry run to preview changes
python3 ${CLAUDE_SKILL_DIR}/scripts/setup-husky-coverage.py \
--root /path/to/project \
--dry-runWhat Gets Configured
Husky Setup
- Installs Husky if not already present
- Initializes Husky (
bunx husky install) - Creates
.husky/pre-commithook that runs tests with coverage - Adds
preparescript to package.json (if missing)
Test Runner Detection
The skill automatically detects:
- Jest: Uses
jest --coverage --watchAll=falsein pre-commit hook - Vitest: Uses
vitest --coverage --runin pre-commit hook - Mocha: Uses
nycorc8with mocha test command
Coverage Configuration
Jest:
- Creates or updates
jest.config.jsonwithcoverageThreshold - Default thresholds: 80% lines, 75% branches, 80% functions, 80% statements
Vitest:
- Creates or updates
vitest.config.ts/jswith coverage thresholds - Configures v8 coverage provider
- Sets same default thresholds as Jest
Mocha + nyc:
- Creates or updates
.nycrc.jsonwith coverage thresholds - Configures text, html, and lcov reporters
Pre-commit Hook
The created hook:
- Runs tests with coverage before every commit
- Fails the commit if coverage is below threshold (configurable)
- Can skip if no test files are found (optional)
Configuration Options
Command Line Arguments
--root <path>: Project root directory (required)--threshold <number>: Coverage threshold percentage (default: 80)--fail-on-below: Fail commit if coverage below threshold (default: true)--no-fail-on-below: Allow commit even if coverage below threshold--skip-if-no-tests: Skip hook if no test files found--dry-run: Show what would be done without making changes
Configuration File
Create .husky-test-coverage.json in project root:
{
"coverageThreshold": {
"lines": 80,
"branches": 75,
"functions": 80,
"statements": 80
},
"failOnCoverageBelowThreshold": true,
"skipIfNoTests": false
}Package.json Configuration
Alternatively, add to package.json:
{
"huskyTestCoverage": {
"threshold": 80,
"failOnBelow": true
}
}Tech Stack Adaptation
Jest Projects
Detection:
- Checks for
jestin dependencies - Looks for
jest.config.jsorjest.config.json
Configuration:
- Updates or creates
jest.config.jsonwith coverage thresholds - Pre-commit hook:
bun run test -- --coverage --watchAll=false
Example jest.config.json:
{
"coverageThreshold": {
"global": {
"lines": 80,
"branches": 75,
"functions": 80,
"statements": 80
}
}
}Vitest Projects
Detection:
- Checks for
vitestin dependencies - Looks for
vitest.config.tsorvitest.config.js
Configuration:
- Updates or creates Vitest config with coverage thresholds
- Pre-commit hook:
bun run test -- --coverage --run
Example vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
branches: 75,
functions: 80,
statements: 80
}
}
}
})Mocha Projects
Detection:
- Checks for
mochain dependencies - Checks for coverage tool (
nycorc8)
Configuration:
- Creates or updates
.nycrc.jsonfor nyc - Pre-commit hook:
nyc --reporter=text --reporter=html bun run test
Example .nycrc.json:
{
"check-coverage": true,
"lines": 80,
"branches": 75,
"functions": 80,
"statements": 80,
"reporter": ["text", "text-summary", "html", "lcov"]
}Package Manager Support
The skill automatically detects and uses:
- bun:
bun run test(preferred) - yarn:
yarn test - pnpm:
pnpm run test
Workflow
When using this skill:
1. Discover Project Context:
- Scan package.json for test runner and dependencies
- Check existing Husky configuration
- Review existing coverage config files
- Verify test files exist
2. Detect Test Runner:
- Identify Jest, Vitest, or Mocha
- Detect coverage tool (built-in or nyc/c8)
- Determine package manager
3. Setup or Verify Husky:
- Install Husky if missing
- Initialize Husky hooks
- Add prepare script if needed
4. Configure Coverage:
- Create or update coverage configuration
- Set coverage thresholds (default 80%)
- Configure appropriate reporters
5. Create Pre-commit Hook:
- Generate hook script with test command
- Configure to run tests with coverage
- Set enforcement behavior (block or warn)
6. Verify Setup:
- Review generated configuration
- Test hook with a commit
- Adjust thresholds if needed
Integration with Other Skills
This skill works alongside:
| Skill | How It Works Together |
|---|---|
| fullstack-workspace-init | Automatically invoked after scaffolding to set up 80% coverage threshold |
| linter-formatter-init | Both configure Husky; this skill focuses on test coverage, linter-formatter-init focuses on linting/formatting |
| testing-expert | Uses testing patterns and coverage targets from testing-expert skill |
Automatic Setup with fullstack-workspace-init
When using fullstack-workspace-init to scaffold a new project, this skill is automatically applied with:
- Vitest as the test runner
- 80% coverage threshold
- Pre-commit hooks enabled
- GitHub Actions CI/CD integration
You don't need to run this skill separately if you used fullstack-workspace-init.
Manual Integration
If adding to an existing project:
python3 ${CLAUDE_SKILL_DIR}/scripts/setup-husky-coverage.py \
--root /path/to/project \
--threshold 80Troubleshooting
Pre-commit hook not running
# Reinstall Husky
bunx husky install
chmod +x .husky/pre-commitCoverage not being checked
- Verify test command includes coverage flag
- Check coverage configuration file exists and is correct
- Ensure coverage tool is installed (nyc/c8 for Mocha)
Hook fails even when tests pass
- Check coverage thresholds are achievable
- Review coverage report to see what's below threshold
- Consider adjusting thresholds or improving test coverage
Tests run but coverage not enforced
- Verify coverage configuration file has correct thresholds
- Check test runner supports coverage (Jest/Vitest have built-in, Mocha needs nyc/c8)
- Review pre-commit hook script for correct command
Multiple test runners detected
The skill uses the first detected runner in priority order: Vitest > Jest > Mocha
Best Practices
1. Start with reasonable thresholds: Begin with 80% and adjust based on project needs 2. Monitor coverage trends: Use coverage reports to identify gaps 3. Incremental improvement: Gradually increase thresholds as coverage improves 4. Consider context: Some files (utilities, configs) may not need high coverage 5. Use with CI/CD: Pre-commit hooks catch issues early, but CI/CD provides final gate 6. Team alignment: Ensure team understands coverage requirements and goals
Resources
- Husky Documentation: https://typicode.github.io/husky/
- Jest Coverage: https://jestjs.io/docs/configuration#coveragethreshold-object
- Vitest Coverage: https://vitest.dev/config/#coverage
- nyc (Istanbul): https://github.com/istanbuljs/nyc
{
"name": "husky-test-coverage",
"version": "1.0.0",
"description": "Set up or verify Husky git hooks to ensure all tests run and coverage stays above 80% (configurable)",
"author": {
"name": "Ship Shit Dev",
"email": "hello@shipshit.dev",
"url": "https://shipshit.dev"
},
"license": "MIT",
"skills": "."
}
#!/usr/bin/env python3
"""
Husky Test Coverage Setup - Configure Husky git hooks to run tests and enforce coverage thresholds
Usage:
setup-husky-coverage.py --root <path> [options]
Options:
--root Project root directory (required)
--threshold Coverage threshold percentage (default: 80)
--fail-on-below Fail commit if coverage below threshold (default: true)
--skip-if-no-tests Skip hook if no tests found (default: false)
--dry-run Show what would be done without making changes
Examples:
setup-husky-coverage.py --root /path/to/project
setup-husky-coverage.py --root /path/to/project --threshold 85
setup-husky-coverage.py --root /path/to/project --threshold 75 --fail-on-below false
"""
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
# ============================================================================
# Configuration Templates
# ============================================================================
PRE_COMMIT_HOOK_TEMPLATE = """#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
{test_command}
"""
JEST_COVERAGE_CONFIG = {
"coverageThreshold": {
"global": {
"lines": 80,
"branches": 75,
"functions": 80,
"statements": 80
}
}
}
VITEST_COVERAGE_CONFIG = """import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
branches: 75,
functions: 80,
statements: 80
}
}
}
})
"""
NYC_CONFIG = {
"check-coverage": True,
"lines": 80,
"branches": 75,
"functions": 80,
"statements": 80,
"reporter": ["text", "text-summary", "html", "lcov"]
}
# ============================================================================
# Helper Functions
# ============================================================================
def detect_package_manager(root: Path) -> str:
"""Detect which package manager is used in the project."""
if (root / "pnpm-lock.yaml").exists():
return "pnpm"
elif (root / "yarn.lock").exists():
return "yarn"
elif (root / "bun.lockb").exists():
return "bun"
else:
return "npm"
def detect_test_runner(root: Path) -> dict:
"""Detect test runner and coverage tool from package.json."""
pkg_json = root / "package.json"
if not pkg_json.exists():
return {}
try:
pkg = json.loads(pkg_json.read_text())
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
result = {
"runner": None,
"coverage": None,
"test_script": pkg.get("scripts", {}).get("test", None)
}
# Detect test runner
if "vitest" in deps:
result["runner"] = "vitest"
elif "jest" in deps:
result["runner"] = "jest"
elif "mocha" in deps:
result["runner"] = "mocha"
# Detect coverage tool
if "nyc" in deps or "@istanbuljs/nyc-config-typescript" in deps:
result["coverage"] = "nyc"
elif "c8" in deps:
result["coverage"] = "c8"
elif result["runner"] in ["jest", "vitest"]:
result["coverage"] = "builtin"
return result
except Exception as e:
print(f" ⚠️ Error reading package.json: {e}")
return {}
def check_existing_husky(root: Path) -> bool:
"""Check if Husky is already installed and configured."""
pkg_json = root / "package.json"
if not pkg_json.exists():
return False
try:
pkg = json.loads(pkg_json.read_text())
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
if "husky" not in deps:
return False
husky_dir = root / ".husky"
if not husky_dir.exists():
return False
return True
except:
return False
def read_config(root: Path) -> dict:
"""Read configuration from .husky-test-coverage.json or package.json."""
config = {
"threshold": 80,
"fail_on_below": True,
"skip_if_no_tests": False
}
# Check for .husky-test-coverage.json
config_file = root / ".husky-test-coverage.json"
if config_file.exists():
try:
file_config = json.loads(config_file.read_text())
if "coverageThreshold" in file_config:
thresholds = file_config["coverageThreshold"]
# Take the highest threshold as the default
config["threshold"] = max(
thresholds.get("lines", 80),
thresholds.get("branches", 75),
thresholds.get("functions", 80),
thresholds.get("statements", 80)
)
config["fail_on_below"] = file_config.get("failOnCoverageBelowThreshold", True)
config["skip_if_no_tests"] = file_config.get("skipIfNoTests", False)
return config
except Exception as e:
print(f" ⚠️ Error reading .husky-test-coverage.json: {e}")
# Check package.json
pkg_json = root / "package.json"
if pkg_json.exists():
try:
pkg = json.loads(pkg_json.read_text())
if "huskyTestCoverage" in pkg:
htc = pkg["huskyTestCoverage"]
config["threshold"] = htc.get("threshold", 80)
config["fail_on_below"] = htc.get("failOnBelow", True)
except:
pass
return config
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" ⚠️ Command failed: {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, dry_run: bool = False):
"""Update package.json with scripts."""
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())
if "scripts" not in pkg:
pkg["scripts"] = {}
pkg["scripts"].update(scripts)
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_husky(root: Path, dry_run: bool):
"""Set up Husky pre-commit hooks."""
print("\n🪝 Setting up Husky...")
pm = detect_package_manager(root)
install_cmd = {
"npm": ["npm", "install", "-D"],
"pnpm": ["pnpm", "add", "-D"],
"yarn": ["yarn", "add", "-D"],
"bun": ["bun", "add", "-D"]
}[pm]
# Check if husky is already installed
pkg_json = root / "package.json"
husky_installed = False
if pkg_json.exists():
try:
pkg = json.loads(pkg_json.read_text())
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
husky_installed = "husky" in deps
except:
pass
if not husky_installed:
run_command(install_cmd + ["husky"], root, dry_run)
else:
print(" ℹ️ Husky already installed")
# Initialize husky
run_command(["npx", "husky", "install"], root, dry_run)
# Add prepare script to package.json
if pkg_json.exists() and not dry_run:
try:
pkg = json.loads(pkg_json.read_text())
if "scripts" not in pkg:
pkg["scripts"] = {}
if "prepare" not in pkg["scripts"]:
pkg["scripts"]["prepare"] = "husky install"
pkg_json.write_text(json.dumps(pkg, indent=2) + "\n")
print(" ✅ Added prepare script to package.json")
except:
pass
def configure_coverage(root: Path, test_info: dict, threshold: int, dry_run: bool):
"""Configure coverage thresholds based on test runner."""
print(f"\n📊 Configuring coverage threshold ({threshold}%)...")
runner = test_info.get("runner")
coverage = test_info.get("coverage")
if runner == "jest":
# Update jest.config.js or create jest.config.json
jest_config_js = root / "jest.config.js"
jest_config_json = root / "jest.config.json"
config_data = JEST_COVERAGE_CONFIG.copy()
config_data["coverageThreshold"]["global"]["lines"] = threshold
config_data["coverageThreshold"]["global"]["branches"] = max(75, threshold - 5)
config_data["coverageThreshold"]["global"]["functions"] = threshold
config_data["coverageThreshold"]["global"]["statements"] = threshold
if jest_config_js.exists():
print(" ℹ️ jest.config.js exists - manual update may be needed")
print(f" Add coverageThreshold: {json.dumps(config_data['coverageThreshold'], indent=6)}")
elif jest_config_json.exists():
try:
existing = json.loads(jest_config_json.read_text())
existing["coverageThreshold"] = config_data["coverageThreshold"]
write_json(jest_config_json, existing, dry_run)
except:
write_json(jest_config_json, config_data, dry_run)
else:
write_json(jest_config_json, config_data, dry_run)
elif runner == "vitest":
# Update vitest.config.ts or vitest.config.js
vitest_config_ts = root / "vitest.config.ts"
vitest_config_js = root / "vitest.config.js"
config_content = VITEST_COVERAGE_CONFIG.replace("lines: 80", f"lines: {threshold}")
config_content = config_content.replace("branches: 75", f"branches: {max(75, threshold - 5)}")
config_content = config_content.replace("functions: 80", f"functions: {threshold}")
config_content = config_content.replace("statements: 80", f"statements: {threshold}")
if vitest_config_ts.exists() or vitest_config_js.exists():
print(" ℹ️ Vitest config exists - manual update may be needed")
print(f" Add coverage.thresholds to your config")
else:
write_text(vitest_config_ts if (root / "tsconfig.json").exists() else vitest_config_js, config_content, dry_run)
elif runner == "mocha" and coverage == "nyc":
# Create or update .nycrc.json
nyc_config = NYC_CONFIG.copy()
nyc_config["lines"] = threshold
nyc_config["branches"] = max(75, threshold - 5)
nyc_config["functions"] = threshold
nyc_config["statements"] = threshold
nycrc = root / ".nycrc.json"
if nycrc.exists():
try:
existing = json.loads(nycrc.read_text())
existing.update({k: v for k, v in nyc_config.items() if k != "reporter"})
write_json(nycrc, existing, dry_run)
except:
write_json(nycrc, nyc_config, dry_run)
else:
write_json(nycrc, nyc_config, dry_run)
def create_pre_commit_hook(root: Path, test_info: dict, threshold: int, fail_on_below: bool, skip_if_no_tests: bool, dry_run: bool):
"""Create pre-commit hook that runs tests with coverage."""
print("\n🪝 Creating pre-commit hook...")
runner = test_info.get("runner")
coverage = test_info.get("coverage")
test_script = test_info.get("test_script", "test")
pm = detect_package_manager(root)
pm_run = {
"npm": "npm run",
"pnpm": "pnpm run",
"yarn": "yarn",
"bun": "bun run"
}[pm]
# Build test command based on runner
if runner == "jest":
test_cmd = f"{pm_run} {test_script} -- --coverage --watchAll=false"
elif runner == "vitest":
test_cmd = f"{pm_run} {test_script} -- --coverage --run"
elif runner == "mocha":
if coverage == "nyc":
test_cmd = f"nyc --reporter=text --reporter=html {pm_run} {test_script}"
elif coverage == "c8":
test_cmd = f"c8 --reporter=text --reporter=html {pm_run} {test_script}"
else:
print(f" ⚠️ Mocha detected but no coverage tool found. Install nyc or c8.")
test_cmd = f"{pm_run} {test_script}"
else:
print(f" ⚠️ Unknown test runner. Using default test command.")
test_cmd = f"{pm_run} {test_script}"
# Add skip logic if needed
if skip_if_no_tests:
hook_content = f"""#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check if test files exist
if [ -z "$(find . -name '*.test.*' -o -name '*.spec.*' -not -path './node_modules/*' 2>/dev/null)" ]; then
echo "⚠️ No test files found, skipping test coverage check"
exit 0
fi
{test_cmd}
"""
else:
hook_content = PRE_COMMIT_HOOK_TEMPLATE.format(test_command=test_cmd)
husky_dir = root / ".husky"
pre_commit_path = husky_dir / "pre-commit"
# Check if pre-commit hook already exists
if pre_commit_path.exists() and not dry_run:
existing_content = pre_commit_path.read_text()
if "coverage" in existing_content.lower() or test_script in existing_content:
print(" ℹ️ Pre-commit hook already exists with test/coverage commands")
print(" Review the hook manually to ensure it runs tests with coverage")
return
write_text(pre_commit_path, hook_content, dry_run)
if not dry_run:
pre_commit_path.chmod(0o755)
def verify_test_setup(root: Path) -> bool:
"""Verify that test setup exists."""
# Check for test files
test_files = list(root.rglob("*.test.*")) + list(root.rglob("*.spec.*"))
test_files = [f for f in test_files if "node_modules" not in str(f)]
if not test_files:
print(" ⚠️ Warning: No test files found (*.test.* or *.spec.*)")
print(" Consider adding tests before setting up coverage enforcement")
return False
return True
# ============================================================================
# Main
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description="Set up Husky git hooks to run tests and enforce coverage thresholds"
)
parser.add_argument("--root", required=True, help="Project root directory")
parser.add_argument("--threshold", type=int, default=80, help="Coverage threshold percentage (default: 80)")
parser.add_argument("--fail-on-below", action="store_true", default=True, help="Fail commit if coverage below threshold")
parser.add_argument("--no-fail-on-below", dest="fail_on_below", action="store_false", help="Allow commit even if coverage below threshold")
parser.add_argument("--skip-if-no-tests", action="store_true", help="Skip hook if no test files found")
parser.add_argument("--dry-run", action="store_true", help="Show what would be done without making changes")
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 'npm init' first to create a package.json")
sys.exit(1)
# Read configuration
config = read_config(root)
threshold = args.threshold or config["threshold"]
fail_on_below = args.fail_on_below if args.fail_on_below is not None else config["fail_on_below"]
skip_if_no_tests = args.skip_if_no_tests or config["skip_if_no_tests"]
print(f"🚀 Setting up Husky test coverage enforcement")
print(f" Project: {root}")
print(f" Coverage threshold: {threshold}%")
print(f" Fail on below threshold: {fail_on_below}")
if args.dry_run:
print("\n⚠️ DRY RUN MODE - No changes will be made\n")
# Detect test runner
test_info = detect_test_runner(root)
if not test_info.get("runner"):
print("\n⚠️ Warning: No test runner detected (Jest, Vitest, or Mocha)")
print(" Install a test runner first:")
print(" - npm install -D jest")
print(" - npm install -D vitest")
print(" - npm install -D mocha")
sys.exit(1)
print(f" Test runner: {test_info['runner']}")
print(f" Coverage tool: {test_info.get('coverage', 'none')}")
# Verify test setup
if not skip_if_no_tests:
verify_test_setup(root)
# Setup Husky
setup_husky(root, args.dry_run)
# Configure coverage
configure_coverage(root, test_info, threshold, args.dry_run)
# Create pre-commit hook
create_pre_commit_hook(root, test_info, threshold, fail_on_below, skip_if_no_tests, args.dry_run)
print("\n✅ Setup complete!")
print("\nNext steps:")
print(f" 1. Review the pre-commit hook in .husky/pre-commit")
print(f" 2. Ensure your test command runs with coverage enabled")
print(f" 3. Make a commit to test the hook")
if not fail_on_below:
print(f" 4. Note: Hook is set to warn only (won't block commits)")
if __name__ == "__main__":
main()