← Files i-have-headacheARCHIVED FILE

install.sh

9.1 KB · Oct 2, 2026 · 00:34 UTC

↓ Download file

#!/bin/sh
# i-have-headache installer - Claude Code, Codex and Cursor, straight from GitHub.
#
#   curl -fsSL https://raw.githubusercontent.com/ihabkhaled/i-have-headache/main/install.sh | sh
#
# Concise mode, always on. One skill, which is also the one command:
#   Claude Code  /i-have-headache:i-have-headache   plugin + SessionStart hook
#   Codex        $i-have-headache                   skill + a block in AGENTS.md
#   Cursor       /i-have-headache                   skill + an alwaysApply rule
#
# Options: --claude --codex --cursor (default: every one detected), --repo PATH,
# --ref REF, --uninstall. Re-running updates. Removes only what it recognises as
# its own. Environment (testing): HEADACHE_SOURCE, HEADACHE_USER_HOME,
# CODEX_HOME, HEADACHE_CLAUDE_BIN ('none' to skip).

set -eu

NAME="i-have-headache"
REPO_URL="${HEADACHE_REPO_URL:-https://github.com/ihabkhaled/i-have-headache.git}"
USER_HOME="${HEADACHE_USER_HOME:-$HOME}"
CODEX_DIR="${CODEX_HOME:-$USER_HOME/.codex}"
REF="main"; TARGET_REPO=""; UNINSTALL=0; WANT_CLAUDE=0; WANT_CODEX=0; WANT_CURSOR=0
BEGIN_MARK="<!-- i-have-headache:begin"
END_MARK="<!-- i-have-headache:end -->"
MARKER="I have a headache."

usage() {
  cat <<'USAGE'
i-have-headache installer - concise mode, always on, for Claude Code, Codex and Cursor.

  sh install.sh [--claude] [--codex] [--cursor] [--repo PATH] [--ref REF] [--uninstall]

Default: every platform detected on this machine. Re-run to update.
USAGE
}
say() { printf '%s\n' "$*"; }
warn() { printf 'warning: %s\n' "$*" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }

while [ $# -gt 0 ]; do
  case "$1" in
    --claude) WANT_CLAUDE=1; shift ;;
    --codex) WANT_CODEX=1; shift ;;
    --cursor) WANT_CURSOR=1; shift ;;
    --repo) [ $# -ge 2 ] || die "--repo needs a path"; TARGET_REPO="$2"; shift 2 ;;
    --ref) [ $# -ge 2 ] || die "--ref needs a branch or tag"; REF="$2"; shift 2 ;;
    --uninstall) UNINSTALL=1; shift ;;
    -h|--help) usage; exit 0 ;;
    *) die "unknown option: $1 (see --help)" ;;
  esac
done

if [ -n "$TARGET_REPO" ]; then
  [ -d "$TARGET_REPO" ] || die "not a directory: $TARGET_REPO"
  TARGET_REPO=$(CDPATH= cd -- "$TARGET_REPO" && pwd)
fi

find_claude() {
  if [ -n "${HEADACHE_CLAUDE_BIN:-}" ]; then
    [ "$HEADACHE_CLAUDE_BIN" = "none" ] && return 1
    printf '%s' "$HEADACHE_CLAUDE_BIN"; return 0
  fi
  if command -v claude >/dev/null 2>&1; then command -v claude; return 0; fi
  found=""
  for c in "$USER_HOME"/.vscode/extensions/anthropic.claude-code-*/resources/native-binary/claude \
           "$USER_HOME"/.vscode/extensions/anthropic.claude-code-*/resources/native-binary/claude.exe; do
    [ -f "$c" ] && found="$c"
  done
  [ -n "$found" ] || return 1
  printf '%s' "$found"
}
CLAUDE_BIN=$(find_claude || true)

if [ "$WANT_CLAUDE$WANT_CODEX$WANT_CURSOR" = "000" ]; then
  [ -n "$CLAUDE_BIN" ] && WANT_CLAUDE=1
  if command -v codex >/dev/null 2>&1 || [ -d "$CODEX_DIR" ]; then WANT_CODEX=1; fi
  if command -v cursor >/dev/null 2>&1 || [ -d "$USER_HOME/.cursor" ]; then WANT_CURSOR=1; fi
  if [ -n "$TARGET_REPO" ]; then WANT_CODEX=1; WANT_CURSOR=1; fi
  [ "$WANT_CLAUDE$WANT_CODEX$WANT_CURSOR" != "000" ] || die "found none of Claude Code, Codex or Cursor. Name one: --claude, --codex or --cursor."
fi

# --- the source ---------------------------------------------------------------

is_checkout() { [ -f "$1/skills/$NAME/SKILL.md" ] && [ -f "$1/.claude-plugin/plugin.json" ]; }
LOCAL_SOURCE=0; SRC=""
if [ -n "${HEADACHE_SOURCE:-}" ]; then
  is_checkout "$HEADACHE_SOURCE" || die "HEADACHE_SOURCE is not an i-have-headache checkout"
  SRC=$(CDPATH= cd -- "$HEADACHE_SOURCE" && pwd); LOCAL_SOURCE=1
else
  case "$0" in *install.sh)
    here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
    if is_checkout "$here"; then SRC="$here"; LOCAL_SOURCE=1; fi ;;
  esac
fi
if [ -z "$SRC" ] && [ "$UNINSTALL" -eq 0 ] && { [ "$WANT_CODEX" -eq 1 ] || [ "$WANT_CURSOR" -eq 1 ]; }; then
  SRC="$USER_HOME/.i-have-headache/src"
  if command -v git >/dev/null 2>&1; then
    if [ -d "$SRC/.git" ]; then
      git -C "$SRC" fetch --quiet --depth 1 origin "$REF"; git -C "$SRC" checkout --quiet --force FETCH_HEAD
    else
      rm -rf "$SRC"; mkdir -p "$(dirname -- "$SRC")"
      git clone --quiet --depth 1 --branch "$REF" "$REPO_URL" "$SRC"
    fi
  else
    rm -rf "$SRC"; mkdir -p "$SRC"
    curl -fsSL "https://codeload.github.com/ihabkhaled/i-have-headache/tar.gz/$REF" | tar -xz -C "$SRC" --strip-components=1
  fi
  is_checkout "$SRC" || die "the download at $SRC is incomplete"
fi

# The always-on rules: the skill's body, without its frontmatter and without the
# acknowledgement meant for an explicit run. The skill stays the only copy.
rules() {
  awk '{ sub(/\r$/, "") } NR == 1 && /^---$/ { f = 1; next } f && /^---$/ { f = 0; next } f { next }
       /^Concise mode is always on/ { exit } { print }' "$SRC/skills/$NAME/SKILL.md"
}

# --- helpers ------------------------------------------------------------------

has_crlf() { [ -f "$1" ] && [ "$(tr -dc '\r' < "$1" | wc -c)" -gt 0 ]; }
to_lf() { awk '{ sub(/\r$/, ""); print }' "$1" > "$1.tmp" && mv "$1.tmp" "$1"; }
to_crlf() { awk '{ sub(/\r$/, ""); printf "%s\r\n", $0 }' "$1" > "$1.tmp" && mv "$1.tmp" "$1"; }
trim_tail() { awk '{ l[NR] = $0 } END { n = NR; while (n > 0 && l[n] == "") n--; for (i = 1; i <= n; i++) print l[i] }' "$1" > "$1.tmp" && mv "$1.tmp" "$1"; }
strip_block() {
  awk -v b="$BEGIN_MARK" -v e="$END_MARK" 'index($0, b) == 1 { s = 1; next } s && index($0, e) == 1 { s = 0; next } !s { print }' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
}

write_block() {
  file="$1"; mkdir -p "$(dirname -- "$file")"
  crlf=0; if has_crlf "$file"; then crlf=1; to_lf "$file"; fi
  if [ -f "$file" ]; then strip_block "$file"; trim_tail "$file"; fi
  {
    if [ -s "$file" ]; then printf '\n'; fi
    printf '%s - installed by i-have-headache; reinstall to update. Replaced on reinstall. -->\n' "$BEGIN_MARK"
    rules
    printf '%s\n' "$END_MARK"
  } >> "$file"
  if [ "$crlf" -eq 1 ]; then to_crlf "$file"; fi
  say "wrote the concise-mode block in $file"
  override="$(dirname -- "$file")/AGENTS.override.md"
  if { [ -n "$TARGET_REPO" ] && [ -e "$override" ]; } || [ -s "$override" ]; then
    warn "$override exists; Codex reads it INSTEAD of AGENTS.md there, so merge the block into it."
  fi
}

remove_block() {
  file="$1"; [ -f "$file" ] || return 0; grep -q "$BEGIN_MARK" "$file" || return 0
  crlf=0; if has_crlf "$file"; then crlf=1; to_lf "$file"; fi
  strip_block "$file"
  if [ -z "$(tr -d ' \t\r\n' < "$file")" ]; then rm -f "$file"; say "removed $file"; return 0; fi
  trim_tail "$file"
  if [ "$crlf" -eq 1 ]; then to_crlf "$file"; fi
  say "removed the concise-mode block from $file"
}

remove_ours() {
  for f in "$SKILLS_ROOT/$NAME/SKILL.md" "$CODEX_DIR/prompts/$NAME.md"; do
    [ -f "$f" ] && grep -q "$MARKER" "$f" || continue
    case "$f" in
      */SKILL.md) rm -rf "$(dirname -- "$f")"; say "removed $(dirname -- "$f")" ;;
      *) rm -f "$f"; say "removed $f (old Codex prompt - it would be a second entry)" ;;
    esac
  done
}

if [ -n "$TARGET_REPO" ]; then
  SKILLS_ROOT="$TARGET_REPO/.agents/skills"; CONTRACT="$TARGET_REPO/AGENTS.md"
  RULE="$TARGET_REPO/.cursor/rules/$NAME.mdc"; SCOPE="project"
else
  SKILLS_ROOT="$USER_HOME/.agents/skills"; CONTRACT="$CODEX_DIR/AGENTS.md"
  RULE="$USER_HOME/.cursor/rules/$NAME.mdc"; SCOPE="user"
fi
claude_run() { if [ -n "$TARGET_REPO" ]; then (cd "$TARGET_REPO" && "$CLAUDE_BIN" "$@"); else "$CLAUDE_BIN" "$@"; fi; }

if [ "$UNINSTALL" -eq 1 ]; then
  if [ "$WANT_CLAUDE" -eq 1 ] && [ -n "$CLAUDE_BIN" ]; then
    claude_run plugin uninstall "$NAME@$NAME" --scope "$SCOPE" || warn "claude: $NAME was not installed"
  fi
  if [ "$WANT_CODEX" -eq 1 ] || [ "$WANT_CURSOR" -eq 1 ]; then remove_ours; fi
  if [ "$WANT_CODEX" -eq 1 ]; then remove_block "$CONTRACT"; fi
  if [ "$WANT_CURSOR" -eq 1 ] && [ -f "$RULE" ] && grep -q "$MARKER" "$RULE"; then rm -f "$RULE"; say "removed $RULE"; fi
  for d in "$SKILLS_ROOT" "$(dirname -- "$SKILLS_ROOT")" "$(dirname -- "$RULE")" "$(dirname -- "$(dirname -- "$RULE")")"; do
    rmdir "$d" 2>/dev/null || true
  done
  say "i-have-headache uninstalled."; exit 0
fi

if [ "$WANT_CLAUDE" -eq 1 ]; then
  if [ -z "$CLAUDE_BIN" ]; then
    warn "Claude Code CLI not found. In VS Code: /plugins -> Marketplaces -> add $REPO_URL -> install."
  else
    if [ "$LOCAL_SOURCE" -eq 1 ]; then MARKET="$SRC"; else MARKET="$REPO_URL#$REF"; fi
    claude_run plugin marketplace add "$MARKET" --scope "$SCOPE"
    claude_run plugin install "$NAME@$NAME" --scope "$SCOPE" || true
    claude_run plugin update "$NAME@$NAME" --scope "$SCOPE" >/dev/null 2>&1 || true
    say "Claude Code: installed ($SCOPE scope). Restart Claude."
  fi
fi

if [ "$WANT_CODEX" -eq 1 ] || [ "$WANT_CURSOR" -eq 1 ]; then
  remove_ours
  mkdir -p "$SKILLS_ROOT"
  cp -R "$SRC/skills/$NAME" "$SKILLS_ROOT/$NAME"
  say "installed the skill to $SKILLS_ROOT/$NAME"
fi
if [ "$WANT_CODEX" -eq 1 ]; then write_block "$CONTRACT"; fi
if [ "$WANT_CURSOR" -eq 1 ]; then
  mkdir -p "$(dirname -- "$RULE")"
  { printf -- '---\ndescription: i-have-headache - concise mode, always on\nalwaysApply: true\n---\n\n'; rules; } > "$RULE"
  say "wrote $RULE"
fi
say "Done. Concise mode is always on. Re-run to update; --uninstall to remove."

SHA-256: 7c0eb8b42b5f8638ced9871d1bd9678bc35ba80c20dc45899e3aebcc2fdaa0c3