#!/usr/bin/env bash set -euo pipefail # Pre-commit hook: keeps dist/ in sync with src/. # # Why: vendor/qmd/dist/ is tracked in git. Consumer machines fetch the # repo and use dist/ AS-IS without rebuilding (cli/build.sh and the fleet # pull_and_build_and_deploy.sh both consume the pre-built JS). A commit # that updates src/*.ts without committing the matching dist/ will ship a # CLI that crashes with MODULE_NOT_FOUND on any new import path. # Reference: i-9l2ueyyz (post-mortem of i-qkarfffa Stage-3 dist/embedding/ # catch-up bug). # # Behaviour: # 1. If no staged file matches ^src/.*\.ts$ -> exit 0 (fast path for # docs/config-only commits). # 2. Otherwise run `npm run build` (which runs tsc + chmod the CLI shim). # 3. Then `git diff --quiet -- dist/` -- if dist/ changed as a result of # the rebuild AND those changes are not staged, FAIL LOUDLY with a # message naming the drifted files and the recovery command. # # Bypass: `git commit --no-verify` (standard git escape hatch). # --- collect staged src/*.ts paths ----------------------------------------- mapfile -t STAGED_SRC < <(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/.*\.ts$' || true) if [[ "${#STAGED_SRC[@]}" -eq 0 ]]; then # Nothing under src/ in this commit -- no rebuild required. exit 0 fi echo "pre-commit: ${#STAGED_SRC[@]} staged src/*.ts file(s) detected -- rebuilding dist/" >&2 # --- snapshot dist/ before rebuild so we can detect rebuild-induced drift -- # Use SHA-256 of git ls-tree-style content (working tree) for every dist/ file # we care about. Cheap enough for ~75 files. DIST_SHA_BEFORE=$(find dist -type f \( -name '*.js' -o -name '*.d.ts' -o -name '*.js.map' -o -name '*.d.ts.map' \) -print0 \ | sort -z \ | xargs -0 sha256sum \ | sha256sum \ | awk '{print $1}') # --- run build -- this also gates on tsc errors ---------------------------- if ! npm run --silent build >/tmp/qmd-pre-commit-build.log 2>&1; then echo "" >&2 echo "pre-commit: npm run build FAILED" >&2 echo "" >&2 echo "Build output (last 40 lines):" >&2 tail -n 40 /tmp/qmd-pre-commit-build.log >&2 echo "" >&2 echo "Full log: /tmp/qmd-pre-commit-build.log" >&2 exit 1 fi DIST_SHA_AFTER=$(find dist -type f \( -name '*.js' -o -name '*.d.ts' -o -name '*.js.map' -o -name '*.d.ts.map' \) -print0 \ | sort -z \ | xargs -0 sha256sum \ | sha256sum \ | awk '{print $1}') # --- determine drift relative to git index --------------------------------- # `git diff --quiet -- dist/` exits 0 if working tree dist/ matches index. # Non-zero means dist/ has changes that are NOT staged. if git diff --quiet -- dist/; then # dist/ matches index -- everything is in sync, commit may proceed. exit 0 fi # Drift detected. Print the offending files + recovery command. echo "" >&2 echo "==========================================================================" >&2 echo "ABORT: dist/ out of sync with src/" >&2 echo "==========================================================================" >&2 echo "" >&2 echo "Your commit modifies src/*.ts but dist/ is not in sync after rebuild." >&2 echo "Shipping this commit would push a broken @oivo/qmd to consumer machines" >&2 echo "(see i-9l2ueyyz / i-qkarfffa Stage-3 retrospective)." >&2 echo "" >&2 echo "Drifted files:" >&2 git diff --name-only -- dist/ | sed 's/^/ /' >&2 echo "" >&2 if [[ "$DIST_SHA_BEFORE" == "$DIST_SHA_AFTER" ]]; then # The rebuild produced the SAME dist/ as before, but git still reports # drift -- this means dist/ was already drifted before this commit # (someone hand-edited or forgot to stage a previous build). Tell them. echo "Note: rebuild produced no changes -- dist/ was already drifted before this" >&2 echo "commit. Stage the drift OR revert dist/ to HEAD before retrying:" >&2 echo "" >&2 echo " # Option A: include the drift in this commit" >&2 echo " git add dist/ && git commit # retry" >&2 echo "" >&2 echo " # Option B: discard the drift (use if dist/ was hand-edited)" >&2 echo " git checkout HEAD -- dist/ && git commit # retry" >&2 else # The rebuild itself produced new dist/ content. User just needs to stage # the freshly-built dist/. echo "Recovery (run, then retry the commit):" >&2 echo "" >&2 echo " git add dist/" >&2 echo " git commit # retry" >&2 fi echo "" >&2 echo "Bypass (NOT recommended -- ships broken @oivo/qmd to fleet):" >&2 echo " git commit --no-verify" >&2 echo "" >&2 exit 1