Skip to content

Instantly share code, notes, and snippets.

@sagarpanchal
Last active April 1, 2025 12:41
Show Gist options
  • Save sagarpanchal/f0aa2e1e6fbf4af79a86d85c8ff6cb4f to your computer and use it in GitHub Desktop.
Save sagarpanchal/f0aa2e1e6fbf4af79a86d85c8ff6cb4f to your computer and use it in GitHub Desktop.
Git hooks auto-setup shell script
#!/bin/zsh
# This script automates the process of setting up Husky for an npm package in a git repo.
# It creates the .husky directory, installs Husky, and sets up the pre-commit and post-merge hooks.
# It also updates the package.json with lint-staged configuration.
# Usage: ./setup-husky.sh
set -e
if [ ! -f "$PWD/package.json" ]; then
echo "[ERROR] \`package.json\` not found" && exit 1
elif ! command -v git &>/dev/null; then
echo "[ERROR] \`git\` not found"
exit 1
elif ! command -v node &>/dev/null; then
echo "[ERROR] \`node\` not found"
exit 1
elif ! command -v npm &>/dev/null; then
echo "[ERROR] \`npm\` not found"
exit 1
elif ! command -v npx &>/dev/null; then
echo "[ERROR] \`npx\` not found"
exit 1
fi
GIT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
if [ -z "$GIT_DIR" ] || [ ! -d "$GIT_DIR" ]; then
echo "[ERROR] Not a \`git\` repo" && exit 1
fi
ROOT_DIR=$GIT_DIR
PACKAGE_DIR=$PWD
HUSKY_DIR="$PACKAGE_DIR/.husky"
RELATIVE_PACKAGE_DIR=$(node -p "require('path').relative('$ROOT_DIR', '$PACKAGE_DIR')")
RELATIVE_HUSKY_DIR=$(node -p "require('path').relative('$ROOT_DIR', '$HUSKY_DIR')")
RELATIVE_ROOT_DIR_PACKAGE=$(node -p "require('path').relative('$PACKAGE_DIR', '$ROOT_DIR')")
# Remove any existing Husky setup from the repo
if [ -d "$HUSKY_DIR" ]; then
echo -n "[WARN] Remove existing Husky setup in $RELATIVE_HUSKY_DIR? (y/N) "
read answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
echo "[INFO] Removing existing Husky setup in $RELATIVE_HUSKY_DIR"
rm -rf "$HUSKY_DIR" || {
echo "[ERROR] Failed to remove $RELATIVE_HUSKY_DIR"
exit 1
}
else
exit 1
fi
fi
function aws_login {
local CONFIG_FILE="$HOME/.aws/config"
# Extract SSO session name
local SSO_SESSION
SSO_SESSION=$(awk -F ' = ' '/^\[sso-session / {gsub(/^\[sso-session |\]$/, "", $0); print; exit}' "$CONFIG_FILE")
# Extract profile name and other required details
local PROFILE
PROFILE=$(awk -F ' = ' '/^\[profile / {gsub(/^\[profile |\]$/, "", $0); print; exit}' "$CONFIG_FILE")
local ACCOUNT_ID
ACCOUNT_ID=$(awk -F ' = ' '$1 == "sso_account_id" {print $2}' "$CONFIG_FILE")
local REGION
REGION=$(awk -F ' = ' '$1 == "region" {print $2}' "$CONFIG_FILE")
if [[ -z "$SSO_SESSION" || -z "$PROFILE" || -z "$ACCOUNT_ID" || -z "$REGION" ]]; then
echo "Error: Could not extract required AWS SSO and profile details."
return 1
fi
# Run aws sso login
echo "[INFO] logging into aws sso"
aws sso login --sso-session "$SSO_SESSION"
# Run aws codeartifact login
echo "[INFO] logging into aws codeartifacts for npm"
aws codeartifact login --tool npm --repository NugetNPMRepo --domain oceantg --domain-owner "$ACCOUNT_ID" --profile "$PROFILE" --region "$REGION"
}
# Use the alias
alias aws-login="aws_login"
function npm_wrapper {
local cmd=$1
shift # Remove the first argument (npm or npx) and pass the rest
# Create a unique temporary file for output
local tmp
tmp=$(mktemp /tmp/${cmd}_last_output.XXXXXX)
local exit_code
script -q /dev/null command $cmd "$@" | tee "$tmp"
exit_code=${pipestatus[1]}
# Read and remove the temporary file
local output
output=$(<"$tmp")
rm "$tmp"
# If an error occurred and the output includes "error code E401", run aws-login and retry
if [[ $output == *"E401"* ]]; then
echo "[INFO] 401 detected. Running aws-login"
aws-login
script -q /dev/null command $cmd "$@"
exit_code=$?
fi
return $exit_code
}
# Use the wrapper function for both npm and npx
alias npm="npm_wrapper npm"
alias npx="npm_wrapper npx"
# Install required dev dependencies (typescript is assumed to be already present)
echo "[INFO] Installing required dependencies"
npm install --audit false --loglevel error --save-dev husky lint-staged || {
echo "[ERROR] Failed to install dev dependencies"
exit 1
}
if [ "$ROOT_DIR" != "$PACKAGE_DIR" ]; then
cd "$ROOT_DIR" || exit 1
npx husky "$HUSKY_DIR" || {
echo "[ERROR] Failed to initialize Husky"
exit 1
}
else
npx husky || {
echo "[ERROR] Failed to initialize Husky"
exit 1
}
fi
cd "$PACKAGE_DIR" || exit 1
git add "$PACKAGE_DIR/package.json" "$PACKAGE_DIR/package-lock.json"
# Create the .husky/pre-commit and .husky/post-merge files
if [ "$ROOT_DIR" != "$PACKAGE_DIR" ]; then
PRE_COMMIT_CMD=$(
cat <<EOF
cd './$RELATIVE_PACKAGE_DIR' && \\
npx tsc --noEmit && \\
npx lint-staged
EOF
)
POST_MERGE_CMD=$(
cat <<EOF
cd './$RELATIVE_PACKAGE_DIR' && \\
npm install && \\
npx tsc --noEmit
EOF
)
else
PRE_COMMIT_CMD=$(
cat <<EOF
npx tsc --noEmit && \\
npx lint-staged
EOF
)
POST_MERGE_CMD=$(
cat <<EOF
npm install && \\
npx tsc --noEmit
EOF
)
fi
# Create the .husky/install.mjs file with the correct command
echo "[INFO] Adding .husky/install.mjs"
cat <<EOF >"$HUSKY_DIR/install.mjs"
if (process.env.CI === 'true' || process.env.NODE_ENV === 'production') process.exit(0);
const husky = (await import('husky')).default;
EOF
if [ "$ROOT_DIR" != "$PACKAGE_DIR" ]; then
cat <<EOF >>"$HUSKY_DIR/install.mjs"
process.chdir('$RELATIVE_ROOT_DIR_PACKAGE');
console.log(husky('./$RELATIVE_HUSKY_DIR'));
EOF
else
cat <<EOF >>"$HUSKY_DIR/install.mjs"
console.log(husky());
EOF
fi
git add "$HUSKY_DIR/install.mjs"
# Create pre-commit hook using the officially recommended command
echo "[INFO] Setting up pre-commit hook"
echo "$PRE_COMMIT_CMD" >"$HUSKY_DIR/pre-commit"
chmod +x "$HUSKY_DIR/pre-commit"
git add "$HUSKY_DIR/pre-commit"
git update-index --chmod=+x "$HUSKY_DIR/pre-commit"
# Create post-merge hook to run npm install
echo "[INFO] Setting up post-merge hook"
echo "$POST_MERGE_CMD" >"$HUSKY_DIR/post-merge"
chmod +x "$HUSKY_DIR/post-merge"
git add "$HUSKY_DIR/post-merge"
git update-index --chmod=+x "$HUSKY_DIR/post-merge"
# Update package.json with lint-staged configuration.
# JavaScript/TypeScript files run prettier and prettier
# Markdown/JSON files are formatted with Prettier.
echo "[INFO] Updating package.json with lint-staged configuration"
node <<EOF
const fs = require('fs');
const path = require('path');
const packageJsonPath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
delete packageJson['scripts']['prepare'];
packageJson['scripts'] = {
"prepare": "node .husky/install.mjs",
...packageJson['scripts'],
}
packageJson['lint-staged'] = {
"(src)/**/*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
],
"(src)/**/*.{md,json}": [
"prettier --write"
],
};
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
EOF
# shellcheck disable=SC2181
if [ $? -ne 0 ]; then exit 1; fi
git add "$PACKAGE_DIR/package.json"
cat <<EOF
[INFO] ROOT_DIR: $ROOT_DIR
[INFO] PACKAGE_DIR: $PACKAGE_DIR
[INFO] HUSKY_DIR: $HUSKY_DIR
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment