doctor.sh

Project health check

Tier 2designengineering

Find broken local setup and missing generated files. Give new contributors a clear list of issues to fix.

Add to your project
Add "Project health check" from ntent to this repo.

Fetch https://ntent.app/r/f/doctor as plain text. Write it verbatim to scripts/doctor.sh. Then make it executable.

Then check it: Run it on a fresh clone. Every line is a pass, or names the one thing to fix.

Reads https://ntent.app/r/f/doctor

Read the code
doctor.shscripts/doctor.sh · make it executable
159 lines
#!/usr/bin/env bash
#
# Preflight for a fresh clone. Reports what is installed, what is missing, and
# the exact command that fixes each gap.
#
# THREE RULES, and they are what make the difference between a doctor script
# people run and one they ignore.
#
# 1. It never exits non-zero. A doctor that fails is a gate, and this is not a
#    gate. It is a diagnostic you run when something is wrong, and it has to
#    finish and print everything even when half the machine is missing.
#
# 2. Sections are labelled by role. A designer who only runs the web preview
#    should be able to see at a glance that the database section does not apply
#    to them. Without this, every missing item reads as "you are broken".
#
# 3. Every failure line contains its remedy. Not "pnpm missing", but
#    "pnpm missing. Run: npm i -g pnpm". The whole point is that the person
#    reading it does not have to go and ask someone.
#
# Run:  pnpm doctor

cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" || exit 0

# ---------------------------------------------------------------- CONFIG ---
# Build outputs that are GENERATED and NOT committed, so a fresh clone does not
# have them.
#
# This is the nastiest gap on the whole list and it earns its own section. The
# app STARTS without them. It just renders wrong, so the person debugging reads
# it as a broken app and goes looking in the components, not at a build step
# they never ran. Nothing else here fails that quietly.
#
# One entry per line, four fields separated by | :
#
#   path | a string every real build contains | the command that builds it | the symptom when it is absent
#
# The second field is what separates "the file is there" from "the file is a
# build". A half-written or truncated output passes an existence test and fails
# everything after it.
#
# WHAT THIS DOES NOT CHECK. Whether the build is CURRENT. A file generated
# before the last change to its source is present, valid and wrong, and that is
# session-status.sh's job, because it is a thing that goes stale during a
# session rather than a thing a fresh clone is missing.
#
# An empty list is fine and normal. The section prints nothing at all when there
# is nothing in it, so a repo with no generated artefacts sees exactly what it
# saw before.
GENERATED=(
  # "src/styles/tokens.css|@theme|pnpm tokens:build|every colour, size and radius resolves to nothing and every page renders as unstyled text on white"
  # "src/generated/schema.ts|export type|pnpm codegen|every typed query is an implicit any and typecheck passes on nonsense"
)

ok()   { printf "  [ ok ] %s\n" "$1"; }
warn() { printf "  [note] %s\n" "$1"; }
bad()  { printf "  [MISS] %s\n" "$1"; }

echo ""
echo "Checking your machine"
echo ""

# ------------------------------------------------------------------ CORE ---
echo "Core, needed for everything:"

if command -v node >/dev/null 2>&1; then
  ver=$(node -v); major=${ver#v}; major=${major%%.*}
  if [ "${major:-0}" -ge 20 ]; then ok "node $ver"
  else warn "node $ver is old. Install 20 or newer: https://nodejs.org or use nvm"; fi
else
  bad "node missing. Install Node 20+: https://nodejs.org"
fi

command -v pnpm >/dev/null 2>&1 \
  && ok "pnpm $(pnpm -v)" \
  || bad "pnpm missing. Run: npm i -g pnpm"

command -v git >/dev/null 2>&1 \
  && ok "git $(git --version | awk '{print $3}')" \
  || bad "git missing. Install Xcode command line tools: xcode-select --install"

if [ -d node_modules ]; then ok "dependencies installed"
else warn "node_modules missing. Run: pnpm install"; fi

# Hooks. This one is easy to miss and it silently disables every gate, so it is
# worth checking explicitly rather than assuming `prepare` ran.
hooks_path="$(git config core.hooksPath 2>/dev/null)"
if [ "$hooks_path" = "scripts/git-hooks" ]; then
  ok "git hooks wired (core.hooksPath = scripts/git-hooks)"
else
  bad "git hooks NOT wired, so no checks run on commit. Run: pnpm install (or: git config core.hooksPath scripts/git-hooks)"
fi

# ------------------------------------------------------------- GENERATED ---
# Silent when the CONFIG list above is empty. See rule 2: a section that does
# not apply to you should not read as something you are missing.
if [ "${#GENERATED[@]}" -gt 0 ]; then
  echo ""
  echo "Generated files. Not committed, so a fresh clone has to build them once:"

  for entry in "${GENERATED[@]}"; do
    IFS='|' read -r gen_path gen_marker gen_cmd gen_symptom <<EOF
$entry
EOF
    if [ ! -f "$gen_path" ]; then
      bad "$gen_path missing. The app will still start, and $gen_symptom. Run: $gen_cmd"
    elif ! grep -q -- "$gen_marker" "$gen_path" 2>/dev/null; then
      # Present but not a build: an interrupted or failed generator leaves a
      # file behind, and every existence check in the world says it is fine.
      bad "$gen_path exists but does not contain '$gen_marker', so it is empty or truncated rather than built. Run: $gen_cmd"
    else
      ok "$gen_path built ($(wc -l < "$gen_path" | tr -d ' ') lines)"
    fi
  done
fi

# --------------------------------------------------------------- SECRETS ---
echo ""
echo "Environment, needed to boot the app:"

if [ -f .env.local ]; then
  missing=""
  while IFS= read -r line; do
    case "$line" in ''|\#*) continue;; esac
    name="${line%%=*}"
    grep -q "^${name}=" .env.local 2>/dev/null || missing="${missing} ${name}"
  done < .env.example
  if [ -z "$missing" ]; then ok ".env.local has every variable from .env.example"
  else bad ".env.local is missing:${missing}. Ask the team, or see .env.example for where each one comes from"; fi
else
  bad ".env.local missing. Run: cp .env.example .env.local, then fill it in"
fi

# -------------------------------------------------------------- OPTIONAL ---
echo ""
echo "Optional, only if you work on these:"

command -v docker >/dev/null 2>&1 \
  && ok "docker present (local database)" \
  || warn "docker not found. Only needed to run the database locally"

command -v gh >/dev/null 2>&1 \
  && ok "gh present (pull requests from the terminal)" \
  || warn "gh not found. Only needed for PR commands. brew install gh"

if [ -f package.json ] && grep -q '"playwright"' package.json 2>/dev/null; then
  [ -d ~/Library/Caches/ms-playwright ] || [ -d ~/.cache/ms-playwright ] \
    && ok "playwright browsers installed" \
    || warn "playwright browsers not installed. Run: pnpm exec playwright install"
fi

# ------------------------------------------------------------------ HINT ---
echo ""
echo "If everything above is [ ok ] or [note], run: pnpm dev"
echo "For design work you only need the Core section."
echo ""

exit 0

Success check: Run it on a fresh clone. Every line is a pass, or names the one thing to fix.

Included in