← Files AkinatorARCHIVED FILE
install.sh
13.1 KB · Oct 3, 2026 · 06:33 UTC
#!/bin/sh
# Akinator installer - Claude Code, Codex and Cursor, straight from GitHub.
#
# curl -fsSL https://raw.githubusercontent.com/ihabkhaled/akinator-ai/main/install.sh | sh
# curl -fsSL https://raw.githubusercontent.com/ihabkhaled/akinator-ai/main/install.sh | sh -s -- --repo .
#
# Akinator is ONE skill with ONE command on every platform:
# Claude Code /akinator:everything plugin, always-on through its SessionStart hook
# Codex $akinator the skill + an always-on block in AGENTS.md
# Cursor /akinator the skill + an alwaysApply rule
#
# Codex and Cursor both read skills from ~/.agents/skills (or <repo>/.agents/skills),
# so one installed folder serves both.
#
# Options:
# --claude --codex --cursor choose platforms (default: every one detected here)
# --repo PATH install into one repository instead of your user
# profile (Claude project scope, <repo>/.agents/skills,
# <repo>/AGENTS.md, <repo>/.cursor/rules)
# --ref REF a branch or tag of the GitHub repository (default main)
# --uninstall remove everything this installer put in place
# -h, --help this text
#
# Re-running updates in place. The installer only ever removes what it can
# recognise as its own: skill folders carrying the Akinator banner, the marked
# akinator:begin/akinator:end block, and its own rule file.
#
# Environment (mostly for testing): AKINATOR_SOURCE (use this checkout instead of
# downloading), AKINATOR_USER_HOME (instead of $HOME), CODEX_HOME,
# AKINATOR_CLAUDE_BIN (the claude executable to use), AKINATOR_REPO_URL.
set -eu
REPO_URL="${AKINATOR_REPO_URL:-https://github.com/ihabkhaled/akinator-ai.git}"
USER_HOME="${AKINATOR_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="<!-- akinator:begin"
END_MARK="<!-- akinator:end -->"
usage() {
cat <<'USAGE'
Akinator installer - one skill, one command, on Claude Code, Codex and Cursor.
sh install.sh [--claude] [--codex] [--cursor] [--repo PATH] [--ref REF] [--uninstall]
--claude --codex --cursor choose platforms (default: every one detected here)
--repo PATH install into one repository instead of your user profile
--ref REF a branch or tag of the GitHub repository (default main)
--uninstall remove everything this installer put in place
One entry point per platform: /akinator:everything (Claude Code),
$akinator (Codex), /akinator (Cursor). 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
# --- the Claude CLI: on PATH, or the one bundled inside the VS Code extension ---
find_claude() {
if [ -n "${AKINATOR_CLAUDE_BIN:-}" ]; then
[ "$AKINATOR_CLAUDE_BIN" = "none" ] && return 1
printf '%s' "$AKINATOR_CLAUDE_BIN"; return 0
fi
if command -v claude >/dev/null 2>&1; then command -v claude; return 0; fi
found=""
for candidate 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 "$candidate" ] && found="$candidate"
done
[ -n "$found" ] || return 1
printf '%s' "$found"
}
CLAUDE_BIN=$(find_claude || true)
# --- which platforms --------------------------------------------------------
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
# A repository install is for whoever opens it next, with any tool.
if [ -n "$TARGET_REPO" ]; then WANT_CODEX=1; WANT_CURSOR=1; fi
if [ "$WANT_CLAUDE$WANT_CODEX$WANT_CURSOR" = "000" ]; then
die "found none of Claude Code, Codex or Cursor. Name one: --claude, --codex or --cursor."
fi
fi
# --- the source: this checkout, or a download --------------------------------
is_checkout() { [ -f "$1/skills/everything/SKILL.md" ] && [ -f "$1/.agents/skills/akinator/SKILL.md" ]; }
LOCAL_SOURCE=0
SRC=""
if [ -n "${AKINATOR_SOURCE:-}" ]; then
is_checkout "$AKINATOR_SOURCE" || die "AKINATOR_SOURCE is not an Akinator checkout: $AKINATOR_SOURCE"
SRC=$(CDPATH= cd -- "$AKINATOR_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/.akinator/src"
if command -v git >/dev/null 2>&1; then
if [ -d "$SRC/.git" ]; then
say "updating $SRC"
git -C "$SRC" fetch --quiet --depth 1 origin "$REF"
git -C "$SRC" checkout --quiet --force FETCH_HEAD
else
say "downloading Akinator ($REF) to $SRC"
rm -rf "$SRC"; mkdir -p "$(dirname -- "$SRC")"
git clone --quiet --depth 1 --branch "$REF" "$REPO_URL" "$SRC"
fi
else
archive="https://codeload.github.com/ihabkhaled/akinator-ai/tar.gz/$REF"
say "downloading Akinator ($REF) to $SRC"
rm -rf "$SRC"; mkdir -p "$SRC"
if command -v curl >/dev/null 2>&1; then curl -fsSL "$archive" | tar -xz -C "$SRC" --strip-components=1
elif command -v wget >/dev/null 2>&1; then wget -qO- "$archive" | tar -xz -C "$SRC" --strip-components=1
else die "need git, curl or wget to download Akinator"; fi
fi
is_checkout "$SRC" || die "the download at $SRC is not a complete Akinator checkout"
fi
# --- helpers ----------------------------------------------------------------
# Only ever touch what is recognisably ours.
is_ours() { [ -f "$1/SKILL.md" ] && grep -q -e "Akinator plugin" -e "build_codex_pack.py" "$1/SKILL.md"; }
remove_our_skills() {
root="$1"
[ -d "$root" ] || return 0
for dir in "$root"/akinator "$root"/akinator-*; do
[ -d "$dir" ] || continue
if is_ours "$dir"; then rm -rf "$dir"; say "removed $dir"; fi
done
}
strip_block() {
file="$1"
[ -f "$file" ] || return 0
grep -q "$BEGIN_MARK" "$file" || return 0
tmp="$file.akinator.tmp"
awk -v b="$BEGIN_MARK" -v e="$END_MARK" '
index($0, b) == 1 { skip = 1; next }
skip && index($0, e) == 1 { skip = 0; next }
!skip { print }' "$file" > "$tmp"
mv "$tmp" "$file"
}
# A user's file keeps its own line endings. The block is edited in LF - awk on
# Git Bash drops CR on read, awk on Linux keeps it - and a CRLF file is turned
# back into CRLF afterwards. Without this, installing Akinator rewrote every
# line ending of a Windows user's AGENTS.md, and uninstall could not restore it.
has_crlf() { [ -f "$1" ] && [ "$(tr -dc '\r' < "$1" | wc -c)" -gt 0 ]; }
to_lf() { awk '{ sub(/\r$/, ""); print }' "$1" > "$1.akinator.tmp" && mv "$1.akinator.tmp" "$1"; }
to_crlf() { awk '{ sub(/\r$/, ""); printf "%s\r\n", $0 }' "$1" > "$1.akinator.tmp" && mv "$1.akinator.tmp" "$1"; }
write_block() {
file="$1"
mkdir -p "$(dirname -- "$file")"
crlf=0
if has_crlf "$file"; then crlf=1; to_lf "$file"; fi
strip_block "$file"
block="$file.akinator.block"
{
printf '%s - installed from the Akinator plugin; reinstall to update. Everything up to akinator:end is replaced on reinstall. -->\n' "$BEGIN_MARK"
# the portable contract, minus its own banner comment
awk 'started { print; next } /^-->$/ { started = 1 }' "$SRC/.agents/AGENTS.md"
printf '%s\n' "$END_MARK"
} > "$block"
if [ -s "$file" ]; then
# drop trailing blank lines so reinstalls do not grow the file
awk '{ lines[NR] = $0 } END { n = NR; while (n > 0 && lines[n] == "") n--; for (i = 1; i <= n; i++) print lines[i] }' "$file" > "$file.akinator.tmp"
mv "$file.akinator.tmp" "$file"
printf '\n' >> "$file"
cat "$block" >> "$file"
else
cat "$block" > "$file"
fi
rm -f "$block"
if [ "$crlf" -eq 1 ]; then to_crlf "$file"; fi
say "wrote the Akinator block in $file"
# Codex reads AGENTS.override.md instead of AGENTS.md: globally when it is
# non-empty, in a repository directory whenever it exists at all.
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
# Drop the blank separator line install added, so the file is byte-for-byte
# what it was before Akinator touched it.
awk '{ lines[NR] = $0 } END { n = NR; while (n > 0 && lines[n] == "") n--; for (i = 1; i <= n; i++) print lines[i] }' "$file" > "$file.akinator.tmp"
mv "$file.akinator.tmp" "$file"
if [ "$crlf" -eq 1 ]; then to_crlf "$file"; fi
say "removed the Akinator block from $file"
}
# --- scope --------------------------------------------------------------------
if [ -n "$TARGET_REPO" ]; then
SKILLS_ROOT="$TARGET_REPO/.agents/skills"
CONTRACT_FILE="$TARGET_REPO/AGENTS.md"
CURSOR_RULE="$TARGET_REPO/.cursor/rules/akinator.mdc"
CLAUDE_SCOPE="project"
else
SKILLS_ROOT="$USER_HOME/.agents/skills"
CONTRACT_FILE="$CODEX_DIR/AGENTS.md"
CURSOR_RULE="$USER_HOME/.cursor/rules/akinator.mdc"
CLAUDE_SCOPE="user"
fi
claude_run() {
if [ -n "$TARGET_REPO" ]; then (cd "$TARGET_REPO" && "$CLAUDE_BIN" "$@"); else "$CLAUDE_BIN" "$@"; fi
}
# --- uninstall ----------------------------------------------------------------
if [ "$UNINSTALL" -eq 1 ]; then
if [ "$WANT_CLAUDE" -eq 1 ] && [ -n "$CLAUDE_BIN" ]; then
claude_run plugin uninstall akinator@akinator --scope "$CLAUDE_SCOPE" || warn "claude: akinator@akinator was not installed"
say "Claude Code: uninstalled akinator@akinator ($CLAUDE_SCOPE scope)"
fi
if [ "$WANT_CODEX" -eq 1 ] || [ "$WANT_CURSOR" -eq 1 ]; then remove_our_skills "$SKILLS_ROOT"; fi
if [ "$WANT_CODEX" -eq 1 ]; then remove_block "$CONTRACT_FILE"; fi
if [ "$WANT_CURSOR" -eq 1 ] && [ -f "$CURSOR_RULE" ] && grep -q "Akinator plugin" "$CURSOR_RULE"; then
rm -f "$CURSOR_RULE"; say "removed $CURSOR_RULE"
fi
# Leave no empty directories this installer created. rmdir refuses non-empty
# ones, so anything the user keeps there survives.
for dir in "$SKILLS_ROOT" "$(dirname -- "$SKILLS_ROOT")" \
"$(dirname -- "$CURSOR_RULE")" "$(dirname -- "$(dirname -- "$CURSOR_RULE")")"; do
rmdir "$dir" 2>/dev/null || true
done
say "Akinator uninstalled."
exit 0
fi
# --- install: Claude Code -----------------------------------------------------
if [ "$WANT_CLAUDE" -eq 1 ]; then
if [ -z "$CLAUDE_BIN" ]; then
warn "Claude Code CLI not found. In the VS Code extension: type /plugins, open Marketplaces,"
warn "add $REPO_URL, then install Akinator."
else
if [ "$LOCAL_SOURCE" -eq 1 ]; then MARKET="$SRC"; else MARKET="$REPO_URL#$REF"; fi
# Adding a marketplace whose name already exists replaces its source, so this
# both installs and switches an older registration over.
claude_run plugin marketplace add "$MARKET" --scope "$CLAUDE_SCOPE"
claude_run plugin install akinator@akinator --scope "$CLAUDE_SCOPE" || true
claude_run plugin update akinator@akinator --scope "$CLAUDE_SCOPE" >/dev/null 2>&1 || true
say "Claude Code: akinator@akinator installed ($CLAUDE_SCOPE scope). Restart Claude, then /akinator:everything."
fi
fi
# --- install: the one skill, for Codex and Cursor ---------------------------
if [ "$WANT_CODEX" -eq 1 ] || [ "$WANT_CURSOR" -eq 1 ]; then
remove_our_skills "$SKILLS_ROOT"
mkdir -p "$SKILLS_ROOT"
cp -R "$SRC/.agents/skills/akinator" "$SKILLS_ROOT/akinator"
find "$SKILLS_ROOT/akinator" -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
say "installed the akinator skill to $SKILLS_ROOT/akinator"
fi
if [ "$WANT_CODEX" -eq 1 ]; then
write_block "$CONTRACT_FILE"
say "Codex: \$akinator is the one entry point; the block makes it always on."
fi
if [ "$WANT_CURSOR" -eq 1 ]; then
mkdir -p "$(dirname -- "$CURSOR_RULE")"
cp "$SRC/.agents/cursor/akinator.mdc" "$CURSOR_RULE"
say "wrote $CURSOR_RULE"
say "Cursor: /akinator is the one entry point; the rule makes it always on."
fi
say ""
say "Done. Re-run this installer to update; add --uninstall to remove."
SHA-256: afee5f3bc0670e7d3d5db12cf1b4cbd0c93515d1b9df100ab965ec172ef3dc11