← Files i-have-headacheARCHIVED FILE
install.sh
9.1 KB · Oct 2, 2026 · 00:34 UTC
#!/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