Skip to content

Commit 222e1e4

Browse files
committed
feat: add curl installer with update notifications
Users can now install/upgrade via: curl -fsSL https://raw.githubusercontent.com/anombyte93/prd-taskmaster/master/install.sh | bash Features: - Fresh install and upgrade detection with SKILL.md backup - Copies SKILL.md, script.py, templates/, reference/ to ~/.claude/skills/prd-taskmaster/ - 24h-cached GitHub Releases API update check - Respects CI, NO_UPDATE_CHECK, NO_COLOR env vars - Self-updating: includes install.sh in skill directory Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d4eaf32 commit 222e1e4

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed

install.sh

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
#!/usr/bin/env bash
2+
# ============================================================================
3+
# Universal Claude Code Skill Installer
4+
# ============================================================================
5+
# Usage:
6+
# curl -fsSL https://raw.githubusercontent.com/OWNER/REPO/main/install.sh | bash
7+
# bash install.sh # fresh install or upgrade
8+
# bash install.sh --check-update # check for newer version only
9+
#
10+
# Customize the variables below for your skill repository.
11+
# ============================================================================
12+
13+
set -euo pipefail
14+
15+
# ---------------------------------------------------------------------------
16+
# Skill Configuration (customize these per-repo)
17+
# ---------------------------------------------------------------------------
18+
REPO_OWNER="anombyte93"
19+
REPO_NAME="prd-taskmaster"
20+
SKILL_NAME="prd-taskmaster"
21+
VERSION="3.0.0"
22+
SKILL_DIR="${SKILL_DIR:-${HOME}/.claude/skills/${SKILL_NAME}}"
23+
24+
# ---------------------------------------------------------------------------
25+
# Internal constants
26+
# ---------------------------------------------------------------------------
27+
UPDATES_DIR="${HOME}/.config/claude-skills"
28+
UPDATES_FILE="${UPDATES_DIR}/updates.json"
29+
UPDATE_INTERVAL_SECONDS=86400 # 24 hours
30+
GITHUB_API="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest"
31+
CLONE_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}.git"
32+
33+
# ---------------------------------------------------------------------------
34+
# Colors (disabled when piped or in CI)
35+
# ---------------------------------------------------------------------------
36+
if [[ -t 1 ]] && [[ -z "${CI:-}" ]] && [[ -z "${NO_COLOR:-}" ]]; then
37+
RED='\033[0;31m'
38+
GREEN='\033[0;32m'
39+
YELLOW='\033[0;33m'
40+
CYAN='\033[0;36m'
41+
BOLD='\033[1m'
42+
RESET='\033[0m'
43+
else
44+
RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET=''
45+
fi
46+
47+
# ---------------------------------------------------------------------------
48+
# Helpers
49+
# ---------------------------------------------------------------------------
50+
info() { printf "${CYAN}[info]${RESET} %s\n" "$*"; }
51+
ok() { printf "${GREEN}[ok]${RESET} %s\n" "$*"; }
52+
warn() { printf "${YELLOW}[warn]${RESET} %s\n" "$*"; }
53+
err() { printf "${RED}[error]${RESET} %s\n" "$*" >&2; }
54+
die() { err "$@"; exit 1; }
55+
56+
cleanup() {
57+
if [[ -n "${TMPDIR_SKILL:-}" ]] && [[ -d "${TMPDIR_SKILL}" ]]; then
58+
rm -rf "${TMPDIR_SKILL}"
59+
fi
60+
}
61+
trap cleanup EXIT
62+
63+
require_cmd() {
64+
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
65+
}
66+
67+
# ---------------------------------------------------------------------------
68+
# Update check (callable standalone)
69+
# ---------------------------------------------------------------------------
70+
check_update() {
71+
# Respect CI / explicit opt-out
72+
if [[ -n "${CI:-}" ]] || [[ -n "${NO_UPDATE_CHECK:-}" ]]; then
73+
return 0
74+
fi
75+
76+
require_cmd curl
77+
require_cmd date
78+
79+
mkdir -p "${UPDATES_DIR}"
80+
81+
# Throttle: only check once per UPDATE_INTERVAL_SECONDS
82+
if [[ -f "${UPDATES_FILE}" ]]; then
83+
local last_check
84+
last_check=$(python3 -c "
85+
import json, sys
86+
try:
87+
d = json.load(open('${UPDATES_FILE}'))
88+
print(d.get('skills', {}).get('${SKILL_NAME}', {}).get('last_check', 0))
89+
except Exception:
90+
print(0)
91+
" 2>/dev/null || echo 0)
92+
93+
local now
94+
now=$(date +%s)
95+
local elapsed=$(( now - last_check ))
96+
97+
if [[ ${elapsed} -lt ${UPDATE_INTERVAL_SECONDS} ]]; then
98+
# Within cooldown -- read cached result
99+
local cached_latest
100+
cached_latest=$(python3 -c "
101+
import json
102+
try:
103+
d = json.load(open('${UPDATES_FILE}'))
104+
print(d.get('skills', {}).get('${SKILL_NAME}', {}).get('latest', ''))
105+
except Exception:
106+
print('')
107+
" 2>/dev/null || echo "")
108+
109+
if [[ -n "${cached_latest}" ]] && [[ "${cached_latest}" != "${VERSION}" ]]; then
110+
warn "Update available: ${VERSION} -> ${cached_latest}"
111+
info "Run: curl -fsSL https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/install.sh | bash"
112+
fi
113+
return 0
114+
fi
115+
fi
116+
117+
# Fetch latest release from GitHub API
118+
local api_response
119+
api_response=$(curl -fsSL --max-time 5 \
120+
-H "Accept: application/vnd.github+json" \
121+
"${GITHUB_API}" 2>/dev/null) || {
122+
# Network failure is non-fatal for update checks
123+
return 0
124+
}
125+
126+
local latest_version
127+
latest_version=$(printf '%s' "${api_response}" | python3 -c "
128+
import json, sys
129+
try:
130+
data = json.load(sys.stdin)
131+
tag = data.get('tag_name', '')
132+
# Strip leading 'v' if present
133+
print(tag.lstrip('v'))
134+
except Exception:
135+
print('')
136+
" 2>/dev/null || echo "")
137+
138+
if [[ -z "${latest_version}" ]]; then
139+
return 0
140+
fi
141+
142+
# Write cache
143+
local now
144+
now=$(date +%s)
145+
python3 -c "
146+
import json, os
147+
148+
path = '${UPDATES_FILE}'
149+
try:
150+
data = json.load(open(path))
151+
except Exception:
152+
data = {}
153+
154+
data.setdefault('skills', {})
155+
data['skills']['${SKILL_NAME}'] = {
156+
'last_check': ${now},
157+
'latest': '${latest_version}',
158+
'current': '${VERSION}'
159+
}
160+
161+
with open(path, 'w') as f:
162+
json.dump(data, f, indent=2)
163+
" 2>/dev/null || true
164+
165+
if [[ "${latest_version}" != "${VERSION}" ]]; then
166+
warn "Update available: ${VERSION} -> ${latest_version}"
167+
info "Run: curl -fsSL https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/install.sh | bash"
168+
else
169+
ok "You are on the latest version (${VERSION})"
170+
fi
171+
}
172+
173+
# ---------------------------------------------------------------------------
174+
# Install logic
175+
# ---------------------------------------------------------------------------
176+
install_skill() {
177+
local mode="install"
178+
179+
info "Claude Code Skill Installer"
180+
info "Skill: ${BOLD}${SKILL_NAME}${RESET} v${VERSION}"
181+
printf "\n"
182+
183+
require_cmd git
184+
185+
# ------------------------------------------------------------------
186+
# Detect upgrade vs fresh install
187+
# ------------------------------------------------------------------
188+
if [[ -d "${SKILL_DIR}" ]]; then
189+
mode="upgrade"
190+
local existing_version="unknown"
191+
if [[ -f "${SKILL_DIR}/.version" ]]; then
192+
existing_version=$(head -1 "${SKILL_DIR}/.version" 2>/dev/null || echo "unknown")
193+
fi
194+
info "Existing installation detected (${existing_version})"
195+
info "Mode: upgrade"
196+
197+
# Back up SKILL.md if it exists
198+
if [[ -f "${SKILL_DIR}/SKILL.md" ]]; then
199+
cp "${SKILL_DIR}/SKILL.md" "${SKILL_DIR}/SKILL.md.bak"
200+
ok "Backed up SKILL.md -> SKILL.md.bak"
201+
fi
202+
else
203+
info "Mode: fresh install"
204+
fi
205+
206+
# ------------------------------------------------------------------
207+
# Clone repo to temp directory
208+
# ------------------------------------------------------------------
209+
TMPDIR_SKILL=$(mktemp -d "${TMPDIR:-/tmp}/claude-skill-XXXXXX")
210+
info "Cloning ${REPO_OWNER}/${REPO_NAME}..."
211+
212+
git clone --depth 1 --quiet "${CLONE_URL}" "${TMPDIR_SKILL}/repo" 2>/dev/null \
213+
|| die "Failed to clone repository. Check REPO_OWNER/REPO_NAME and network."
214+
215+
# ------------------------------------------------------------------
216+
# Locate skill source within the repo
217+
# ------------------------------------------------------------------
218+
# Convention: skill files live at repo root OR under a directory named
219+
# after the skill. We check both.
220+
local src_dir="${TMPDIR_SKILL}/repo"
221+
if [[ -d "${TMPDIR_SKILL}/repo/${SKILL_NAME}" ]]; then
222+
src_dir="${TMPDIR_SKILL}/repo/${SKILL_NAME}"
223+
fi
224+
225+
# Verify at minimum SKILL.md exists
226+
if [[ ! -f "${src_dir}/SKILL.md" ]]; then
227+
die "SKILL.md not found in repository (checked ${src_dir}). Cannot install."
228+
fi
229+
230+
# ------------------------------------------------------------------
231+
# Copy skill files to SKILL_DIR
232+
# ------------------------------------------------------------------
233+
mkdir -p "${SKILL_DIR}"
234+
235+
# Core file: SKILL.md (always required)
236+
cp "${src_dir}/SKILL.md" "${SKILL_DIR}/SKILL.md"
237+
ok "Installed SKILL.md"
238+
239+
# Optional: script.py (or any .py entrypoint)
240+
if [[ -f "${src_dir}/script.py" ]]; then
241+
cp "${src_dir}/script.py" "${SKILL_DIR}/script.py"
242+
ok "Installed script.py"
243+
fi
244+
245+
# Optional: templates/ directory
246+
if [[ -d "${src_dir}/templates" ]]; then
247+
rm -rf "${SKILL_DIR}/templates"
248+
cp -r "${src_dir}/templates" "${SKILL_DIR}/templates"
249+
ok "Installed templates/"
250+
fi
251+
252+
# Optional: reference/ directory
253+
if [[ -d "${src_dir}/reference" ]]; then
254+
rm -rf "${SKILL_DIR}/reference"
255+
cp -r "${src_dir}/reference" "${SKILL_DIR}/reference"
256+
ok "Installed reference/"
257+
fi
258+
259+
# Optional: install.sh itself (so the skill is self-updating)
260+
if [[ -f "${src_dir}/install.sh" ]]; then
261+
cp "${src_dir}/install.sh" "${SKILL_DIR}/install.sh"
262+
chmod +x "${SKILL_DIR}/install.sh"
263+
fi
264+
265+
# ------------------------------------------------------------------
266+
# Write .version file
267+
# ------------------------------------------------------------------
268+
local timestamp
269+
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
270+
cat > "${SKILL_DIR}/.version" <<VEOF
271+
${VERSION}
272+
installed: ${timestamp}
273+
mode: ${mode}
274+
repo: ${REPO_OWNER}/${REPO_NAME}
275+
VEOF
276+
ok "Wrote .version (${VERSION}, ${timestamp})"
277+
278+
# ------------------------------------------------------------------
279+
# Success
280+
# ------------------------------------------------------------------
281+
printf "\n"
282+
printf "${GREEN}${BOLD}Successfully %s ${SKILL_NAME} v${VERSION}${RESET}\n" \
283+
"$([ "${mode}" = "upgrade" ] && echo "upgraded" || echo "installed")"
284+
printf " Location: %s\n" "${SKILL_DIR}"
285+
286+
if [[ "${mode}" = "upgrade" ]] && [[ -f "${SKILL_DIR}/SKILL.md.bak" ]]; then
287+
printf " Backup: %s\n" "${SKILL_DIR}/SKILL.md.bak"
288+
fi
289+
290+
printf "\n"
291+
info "To use this skill in Claude Code, reference it with:"
292+
printf " ${CYAN}/%s${RESET}\n" "${SKILL_NAME}"
293+
printf "\n"
294+
}
295+
296+
# ---------------------------------------------------------------------------
297+
# Entrypoint
298+
# ---------------------------------------------------------------------------
299+
main() {
300+
case "${1:-}" in
301+
--check-update|-u)
302+
check_update
303+
;;
304+
--version|-v)
305+
echo "${SKILL_NAME} v${VERSION}"
306+
;;
307+
--help|-h)
308+
printf "Usage: %s [--check-update | --version | --help]\n" "${0##*/}"
309+
printf "\n"
310+
printf " (no args) Install or upgrade the skill\n"
311+
printf " --check-update Check GitHub for a newer release\n"
312+
printf " --version Print current version\n"
313+
printf " --help Show this help\n"
314+
;;
315+
"")
316+
install_skill
317+
check_update
318+
;;
319+
*)
320+
die "Unknown argument: $1 (try --help)"
321+
;;
322+
esac
323+
}
324+
325+
main "$@"

0 commit comments

Comments
 (0)