Added Husky precommit support and added pre-commit hook that automatically downloads and caches the appropriate binary for your platform (no Docker or manual installation required).

This commit is contained in:
Mick Grove 2026-01-30 08:33:59 -08:00
commit 45cab25615
6 changed files with 548 additions and 4 deletions

148
scripts/install-husky.sh Executable file
View file

@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Install Kingfisher as a Husky pre-commit hook
# Usage: ./install-husky.sh [--uninstall]
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: install-husky.sh [OPTIONS]
Adds Kingfisher to your Husky pre-commit hook.
Options:
--uninstall Remove Kingfisher from the Husky pre-commit hook
--use-docker Use Docker instead of local binary (no installation needed)
--auto-install Auto-download kingfisher binary if not present
-h, --help Show this help message
Requirements:
- Node.js project with Husky already initialized
- OR run this script after 'npx husky init'
USAGE
}
UNINSTALL=false
USE_DOCKER=false
AUTO_INSTALL=false
while [[ $# -gt 0 ]]; do
case "$1" in
--uninstall)
UNINSTALL=true
shift
;;
--use-docker)
USE_DOCKER=true
shift
;;
--auto-install)
AUTO_INSTALL=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
# Find Husky directory
find_husky_dir() {
if [[ -d ".husky" ]]; then
echo ".husky"
elif [[ -d ".config/husky" ]]; then
echo ".config/husky"
else
echo ""
fi
}
HUSKY_DIR="$(find_husky_dir)"
if [[ -z "$HUSKY_DIR" ]]; then
echo "Error: Husky directory not found." >&2
echo "Initialize Husky first with: npx husky init" >&2
exit 1
fi
PRE_COMMIT="$HUSKY_DIR/pre-commit"
MARKER="# kingfisher-scan"
# Determine the scan command
if $USE_DOCKER; then
SCAN_CMD='docker run --rm -v "$(pwd)":/src ghcr.io/mongodb/kingfisher:latest scan /src --staged --quiet --no-update-check'
elif $AUTO_INSTALL; then
# Use the auto-download script approach
SCAN_CMD='curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/kingfisher-pre-commit-auto.sh | bash'
else
SCAN_CMD='kingfisher scan . --staged --quiet --no-update-check'
fi
uninstall() {
if [[ -f "$PRE_COMMIT" ]]; then
# Remove kingfisher lines from pre-commit
if grep -q "$MARKER" "$PRE_COMMIT"; then
# Create temp file without kingfisher lines
local tmpfile
tmpfile="$(mktemp)"
grep -v "$MARKER" "$PRE_COMMIT" | grep -v "kingfisher scan\|kingfisher:latest\|kingfisher-pre-commit-auto" > "$tmpfile" || true
mv "$tmpfile" "$PRE_COMMIT"
chmod +x "$PRE_COMMIT"
echo "Kingfisher removed from $PRE_COMMIT"
else
echo "Kingfisher not found in $PRE_COMMIT"
fi
else
echo "No pre-commit hook found at $PRE_COMMIT"
fi
}
install() {
# Create pre-commit if it doesn't exist
if [[ ! -f "$PRE_COMMIT" ]]; then
cat > "$PRE_COMMIT" <<'EOF'
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
EOF
chmod +x "$PRE_COMMIT"
echo "Created $PRE_COMMIT"
fi
# Check if kingfisher is already installed
if grep -q "$MARKER" "$PRE_COMMIT" 2>/dev/null; then
echo "Kingfisher is already configured in $PRE_COMMIT"
return 0
fi
# Append kingfisher scan command
cat >> "$PRE_COMMIT" <<EOF
$MARKER
$SCAN_CMD
EOF
echo "Kingfisher added to $PRE_COMMIT"
if ! $USE_DOCKER && ! $AUTO_INSTALL; then
if ! command -v kingfisher >/dev/null 2>&1; then
echo ""
echo "Note: kingfisher is not installed. Install it with:"
echo " curl -fsSL https://raw.githubusercontent.com/mongodb/kingfisher/main/scripts/install-kingfisher.sh | bash"
echo ""
echo "Or re-run this script with --use-docker or --auto-install"
fi
fi
}
if $UNINSTALL; then
uninstall
else
install
fi

View file

@ -0,0 +1,143 @@
<#
.SYNOPSIS
Kingfisher pre-commit hook with automatic binary download for Windows.
.DESCRIPTION
Downloads and caches the Kingfisher binary, then scans staged changes.
No manual installation required.
.PARAMETER Version
Specific version to download (e.g., "1.76.0" or "v1.76.0").
Defaults to "latest".
.EXAMPLE
./kingfisher-pre-commit-auto.ps1
.EXAMPLE
$env:KINGFISHER_VERSION = "1.76.0"; ./kingfisher-pre-commit-auto.ps1
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$repo = 'mongodb/kingfisher'
$cacheDir = if ($env:KINGFISHER_CACHE_DIR) {
$env:KINGFISHER_CACHE_DIR
} else {
Join-Path $env:LOCALAPPDATA 'kingfisher'
}
$kingfisherBin = Join-Path $cacheDir 'kingfisher.exe'
$versionFile = Join-Path $cacheDir '.version'
$expectedVersion = if ($env:KINGFISHER_VERSION) { $env:KINGFISHER_VERSION } else { 'latest' }
function Get-AssetName {
# Windows only supports x64 currently
return 'kingfisher-windows-x64.zip'
}
function Download-Kingfisher {
param(
[string]$Version
)
$assetName = Get-AssetName
if (-not (Test-Path $cacheDir)) {
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
}
$tempDir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.Guid]::NewGuid().ToString())
try {
if ($Version -eq 'latest') {
$downloadUrl = "https://github.com/$repo/releases/latest/download/$assetName"
Write-Host "Downloading kingfisher (latest) for Windows..." -ForegroundColor Cyan
} else {
# Support both "v1.76.0" and "1.76.0" formats
if (-not $Version.StartsWith('v')) {
$Version = "v$Version"
}
$downloadUrl = "https://github.com/$repo/releases/download/$Version/$assetName"
Write-Host "Downloading kingfisher ($Version) for Windows..." -ForegroundColor Cyan
}
$archivePath = Join-Path $tempDir.FullName $assetName
try {
Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing
} catch {
Write-Error "Failed to download $downloadUrl : $_"
exit 1
}
Write-Host "Extracting archive..." -ForegroundColor Cyan
Expand-Archive -Path $archivePath -DestinationPath $tempDir.FullName -Force
$extractedBinary = Join-Path $tempDir.FullName 'kingfisher.exe'
if (-not (Test-Path $extractedBinary)) {
Write-Error "Binary not found in downloaded archive"
exit 1
}
Copy-Item -Path $extractedBinary -Destination $kingfisherBin -Force
# Store the version we downloaded
if ($Version -eq 'latest') {
try {
$versionOutput = & $kingfisherBin --version 2>$null | Select-Object -First 1
Set-Content -Path $versionFile -Value $versionOutput -NoNewline
} catch {
Set-Content -Path $versionFile -Value 'latest' -NoNewline
}
} else {
Set-Content -Path $versionFile -Value $Version -NoNewline
}
Write-Host "Kingfisher installed to $kingfisherBin" -ForegroundColor Green
}
finally {
if ($tempDir -and (Test-Path $tempDir.FullName)) {
Remove-Item -Path $tempDir.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
function Test-NeedsDownload {
# Binary doesn't exist
if (-not (Test-Path $kingfisherBin)) {
return $true
}
# No version tracking - always use existing binary for 'latest'
if ($expectedVersion -eq 'latest') {
return $false
}
# Check if version matches
if (Test-Path $versionFile) {
$installedVersion = Get-Content -Path $versionFile -Raw
# Normalize version format for comparison
$expectedNormalized = $expectedVersion
if (-not $expectedNormalized.StartsWith('v')) {
$expectedNormalized = "v$expectedNormalized"
}
if ($installedVersion -like "*$expectedNormalized*" -or $installedVersion -eq $expectedVersion) {
return $false
}
}
return $true
}
# Main execution
if (Test-NeedsDownload) {
Download-Kingfisher -Version $expectedVersion
}
# Run kingfisher scan on staged changes
# Pass through any additional arguments
& $kingfisherBin scan . --staged --quiet --no-update-check @args
exit $LASTEXITCODE

View file

@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Kingfisher pre-commit hook with automatic binary download
# This script downloads the appropriate kingfisher binary if not already cached,
# then runs the scan against staged changes.
set -euo pipefail
REPO="mongodb/kingfisher"
CACHE_DIR="${KINGFISHER_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/kingfisher}"
KINGFISHER_BIN="$CACHE_DIR/kingfisher"
VERSION_FILE="$CACHE_DIR/.version"
# Determine the expected version from the pre-commit rev (passed as env var or default to latest)
EXPECTED_VERSION="${KINGFISHER_VERSION:-latest}"
get_platform() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux) platform="linux" ;;
Darwin) platform="darwin" ;;
MINGW*|MSYS*|CYGWIN*) platform="windows" ;;
*) echo "Error: Unsupported OS '$os'" >&2; exit 1 ;;
esac
case "$arch" in
x86_64|amd64) arch_suffix="x64" ;;
arm64|aarch64) arch_suffix="arm64" ;;
*) echo "Error: Unsupported architecture '$arch'" >&2; exit 1 ;;
esac
echo "${platform}-${arch_suffix}"
}
download_kingfisher() {
local platform="$1"
local version="$2"
local ext="tgz"
if [[ "$platform" == windows-* ]]; then
ext="zip"
fi
local asset_name="kingfisher-${platform}.${ext}"
local download_url
if [[ "$version" == "latest" ]]; then
download_url="https://github.com/${REPO}/releases/latest/download/${asset_name}"
else
# Support both "v1.76.0" and "1.76.0" formats
if [[ "$version" != v* ]]; then
version="v${version}"
fi
download_url="https://github.com/${REPO}/releases/download/${version}/${asset_name}"
fi
mkdir -p "$CACHE_DIR"
local tmpdir
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
echo "Downloading kingfisher ($version) for $platform..." >&2
if ! curl -fLsS "$download_url" -o "$tmpdir/$asset_name"; then
echo "Error: Failed to download $download_url" >&2
exit 1
fi
if [[ "$ext" == "zip" ]]; then
unzip -q "$tmpdir/$asset_name" -d "$tmpdir"
local binary_name="kingfisher.exe"
else
tar -C "$tmpdir" -xzf "$tmpdir/$asset_name"
local binary_name="kingfisher"
fi
if [[ ! -f "$tmpdir/$binary_name" ]]; then
echo "Error: Binary not found in downloaded archive" >&2
exit 1
fi
mv "$tmpdir/$binary_name" "$KINGFISHER_BIN"
chmod +x "$KINGFISHER_BIN"
# Store the version we downloaded
if [[ "$version" == "latest" ]]; then
"$KINGFISHER_BIN" --version 2>/dev/null | head -1 > "$VERSION_FILE" || echo "latest" > "$VERSION_FILE"
else
echo "$version" > "$VERSION_FILE"
fi
echo "Kingfisher installed to $KINGFISHER_BIN" >&2
}
needs_download() {
# Binary doesn't exist
if [[ ! -x "$KINGFISHER_BIN" ]]; then
return 0
fi
# No version tracking - always use existing binary for 'latest'
if [[ "$EXPECTED_VERSION" == "latest" ]]; then
return 1
fi
# Check if version matches
if [[ -f "$VERSION_FILE" ]]; then
local installed_version
installed_version="$(cat "$VERSION_FILE")"
# Normalize version format for comparison
local expected_normalized="$EXPECTED_VERSION"
if [[ "$expected_normalized" != v* ]]; then
expected_normalized="v${expected_normalized}"
fi
if [[ "$installed_version" == *"$expected_normalized"* ]] || [[ "$installed_version" == "$EXPECTED_VERSION" ]]; then
return 1
fi
fi
return 0
}
main() {
local platform
platform="$(get_platform)"
if needs_download; then
download_kingfisher "$platform" "$EXPECTED_VERSION"
fi
# Run kingfisher scan on staged changes
exec "$KINGFISHER_BIN" scan . --staged --quiet --no-update-check "$@"
}
main "$@"