set -eu
usage() {
sed -n '4,33p' "$0" | sed 's/^# \{0,1\}//'
}
script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
repo_root=$(CDPATH='' cd -- "$script_dir/.." && pwd)
skills_root="$repo_root/skills"
[ -d "$skills_root" ] || {
echo "error: skills directory not found at $skills_root" >&2
exit 1
}
action=install
layout=per-harness
link_mode=0
force=0
project=""
agents=""
skills=""
explicit_dirs=""
agent_dir() {
harness=$1
if [ -n "$project" ]; then
case "$harness" in
opencode) printf '%s' "$project/.opencode/skills" ;;
claude) printf '%s' "$project/.claude/skills" ;;
codex) printf '%s' "$project/.agents/skills" ;;
esac
return
fi
case "$harness" in
opencode) printf '%s' "${XDG_CONFIG_HOME:-$HOME/.config}/opencode/skills" ;;
claude) printf '%s' "$HOME/.claude/skills" ;;
codex) printf '%s' "$HOME/.agents/skills" ;;
esac
}
while [ $# -gt 0 ]; do
case "$1" in
--uninstall) action=uninstall ;;
--list) action=list ;;
--force) force=1 ;;
--link) link_mode=1 ;;
--project)
shift
[ -n "${1:-}" ] || { echo "error: --project expects a directory" >&2; exit 1; }
project=$1
[ -d "$project" ] || { echo "error: project directory '$project' does not exist" >&2; exit 1; }
;;
--dir)
shift
[ -n "${1:-}" ] || { echo "error: --dir expects a directory" >&2; exit 1; }
explicit_dirs="$explicit_dirs
$1"
;;
--agent)
shift
case "${1:-}" in
opencode | claude | codex) agents="$agents $1" ;;
*)
echo "error: --agent expects opencode, claude, or codex" >&2
exit 1
;;
esac
;;
--skill)
shift
[ -n "${1:-}" ] || { echo "error: --skill expects a name" >&2; exit 1; }
case "$1" in
.|..|*[!A-Za-z0-9._-]*)
echo "error: invalid skill name '$1'" >&2
exit 1
;;
esac
skills="$skills $1"
;;
--layout)
shift
case "${1:-}" in
covering | per-harness) layout=$1 ;;
*)
echo "error: --layout expects covering or per-harness" >&2
exit 1
;;
esac
;;
--help | -h)
usage
exit 0
;;
*)
echo "error: unknown option '$1'" >&2
usage >&2
exit 1
;;
esac
shift
done
selected_agent() {
case " $agents " in *" $1 "*) return 0 ;; *) return 1 ;; esac
}
select_agents_interactively() {
[ -t 0 ] && [ -t 1 ] || {
echo "error: no --agent given and no interactive terminal; pass --agent opencode|claude|codex" >&2
exit 1
}
printf 'Select agent harnesses (comma separated) [opencode,claude,codex]: '
read -r picked
old_ifs=$IFS
IFS=', '
for entry in $picked; do
[ -n "$entry" ] || continue
case "$entry" in
opencode | claude | codex) agents="$agents $entry" ;;
*) echo "error: unknown harness '$entry'" >&2; exit 1 ;;
esac
done
IFS=$old_ifs
[ -n "$agents" ] || { echo "error: no harness selected" >&2; exit 1; }
}
all_skills() {
for dir in "$skills_root"/*/; do
[ -f "$dir/SKILL.md" ] || continue
basename "$dir"
done
}
selected_skills() {
if [ -n "${skills# }" ]; then
printf '%s\n' ${skills# }
return
fi
all_skills
}
require_bundled_skills() {
[ "$action" = install ] || return 0
[ -n "${skills# }" ] || return 0
for name in ${skills# }; do
[ -f "$skills_root/$name/SKILL.md" ] || {
echo "error: unknown skill '$name' (see $skills_root)" >&2
exit 1
}
done
}
destinations() {
if [ -n "$explicit_dirs" ]; then
index=0
printf '%s\n' "$explicit_dirs" | while IFS= read -r dir; do
[ -n "$dir" ] || continue
index=$((index + 1))
printf 'custom-%s\t%s\n' "$index" "$dir"
done
return
fi
has_oc=0
has_cl=0
has_cx=0
if selected_agent opencode; then has_oc=1; fi
if selected_agent claude; then has_cl=1; fi
if selected_agent codex; then has_cx=1; fi
if [ "$layout" = "covering" ]; then
if [ "$has_oc" = 1 ] && [ "$has_cl" = 1 ] && [ "$has_cx" = 1 ]; then
echo "error: opencode scans the claude and codex skill directories, so {opencode,claude,codex} has no duplicate-free placement; select two harnesses, or pass --layout per-harness --force to accept duplicate discovery" >&2
exit 1
fi
if [ "$has_oc" = 1 ] && [ "$has_cl" = 1 ]; then
printf 'shared\t%s\n' "$(agent_dir claude)"
return
fi
if [ "$has_oc" = 1 ] && [ "$has_cx" = 1 ]; then
printf 'shared\t%s\n' "$(agent_dir codex)"
return
fi
fi
if [ "$layout" = "per-harness" ] && [ "$has_oc" = 1 ] && { [ "$has_cl" = 1 ] || [ "$has_cx" = 1 ]; }; then
if [ "$force" != 1 ]; then
echo "error: a per-harness copy for opencode plus claude/codex makes opencode discover the same skill twice; pass --layout covering, or --force to accept duplicate discovery" >&2
exit 1
fi
echo "warning: opencode scans the claude and codex directories; per-harness copies will duplicate discovery" >&2
fi
if [ "$has_oc" = 1 ]; then printf 'opencode\t%s\n' "$(agent_dir opencode)"; fi
if [ "$has_cl" = 1 ]; then printf 'claude\t%s\n' "$(agent_dir claude)"; fi
if [ "$has_cx" = 1 ]; then printf 'codex\t%s\n' "$(agent_dir codex)"; fi
}
receipt_name=.solverforge-skill
link_receipt_prefix=.solverforge-skill-link-
rcpt_tmp=
parse_link_receipt() {
rc_format=
rc_skill=
rc_mode=
rc_target=
[ -f "$1" ] && [ ! -L "$1" ] || return 1
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
format=*) rc_format=${line#format=} ;;
skill=*) rc_skill=${line#skill=} ;;
mode=*) rc_mode=${line#mode=} ;;
target=*) rc_target=${line#target=} ;;
esac
done < "$1"
[ "$rc_format" = "1" ] && [ "$rc_mode" = "link" ] && [ -n "$rc_skill" ] && [ -n "$rc_target" ]
}
managed_copy() {
[ -f "$1/$receipt_name" ] && [ ! -L "$1/$receipt_name" ] || return 1
[ "$(cat "$1/$receipt_name" 2>/dev/null)" = "$2" ]
}
link_receipt_path() {
printf '%s/%s%s' "$1" "$link_receipt_prefix" "$2"
}
managed_link() {
parse_link_receipt "$(link_receipt_path "$1" "$2")" || return 1
[ "$rc_skill" = "$2" ] || return 1
[ -L "$1/$2" ] || return 1
[ "$(readlink "$1/$2")" = "$rc_target" ]
}
has_link_receipt() {
[ -e "$(link_receipt_path "$1" "$2")" ]
}
write_file_atomically() {
target=$1
shift
rcpt_tmp=$(mktemp "${target}.XXXXXX") || {
echo "error: cannot create a temporary file next to $target" >&2
exit 1
}
for line
do
printf '%s\n' "$line" >> "$rcpt_tmp" || {
rm -f "$rcpt_tmp"
rcpt_tmp=
echo "error: cannot write a temporary file next to $target" >&2
exit 1
}
done
mv -f "$rcpt_tmp" "$target" || {
rm -f "$rcpt_tmp"
rcpt_tmp=
echo "error: cannot replace $target" >&2
exit 1
}
rcpt_tmp=
}
install_one() {
skill=$1
dir=$2
src="$skills_root/$skill"
dest="$dir/$skill"
if [ "$action" = list ]; then
if managed_link "$dir" "$skill"; then
printf 'link %-12s %s -> %s\n' "$skill" "$dest" "$(readlink "$dest")"
elif has_link_receipt "$dir" "$skill"; then
printf 'stale-rcpt %-12s %s (receipt without its recorded link)\n' "$skill" "$dest"
elif managed_copy "$dest" "$skill"; then
if diff -r -x "$receipt_name" "$src" "$dest" >/dev/null 2>&1; then
printf 'current %-12s %s\n' "$skill" "$dest"
else
printf 'outdated %-12s %s\n' "$skill" "$dest"
fi
elif [ -e "$dest" ] || [ -L "$dest" ]; then
printf 'foreign %-12s %s (left untouched)\n' "$skill" "$dest"
else
printf 'absent %-12s %s\n' "$skill" "$dest"
fi
return
fi
if [ "$action" = uninstall ]; then
if managed_link "$dir" "$skill"; then
rm -f "$dest" "$(link_receipt_path "$dir" "$skill")"
printf 'removed %-12s %s\n' "$skill" "$dest"
elif has_link_receipt "$dir" "$skill"; then
rm -f "$(link_receipt_path "$dir" "$skill")"
printf 'stale-rcpt %-12s receipt removed; destination left untouched\n' "$skill"
elif managed_copy "$dest" "$skill"; then
rm -rf "$dest"
printf 'removed %-12s %s\n' "$skill" "$dest"
elif [ -e "$dest" ] || [ -L "$dest" ]; then
printf 'untouched %-12s %s (not installed by this installer)\n' "$skill" "$dest"
fi
return
fi
if [ -L "$dest" ]; then
if managed_link "$dir" "$skill"; then
rm -f "$dest" "$(link_receipt_path "$dir" "$skill")"
else
echo "error: $dest is a symlink not managed by this installer" >&2
exit 1
fi
elif [ -e "$dest" ] && ! managed_copy "$dest" "$skill"; then
echo "error: $dest exists and was not installed by this installer" >&2
exit 1
fi
mkdir -p "$dir"
if [ "$link_mode" = 1 ]; then
rm -rf "$dest"
ln -s "$src" "$dest"
write_file_atomically "$(link_receipt_path "$dir" "$skill")" \
"format=1" "skill=$skill" "mode=link" "target=$src"
printf 'linked %-12s %s -> %s\n' "$skill" "$dest" "$src"
return
fi
if [ -d "$dest" ] && managed_copy "$dest" "$skill" && diff -r -x "$receipt_name" "$src" "$dest" >/dev/null 2>&1; then
rm -f "$(link_receipt_path "$dir" "$skill")"
printf 'current %-12s %s\n' "$skill" "$dest"
return
fi
rm -rf "$dest"
rm -f "$(link_receipt_path "$dir" "$skill")"
cp -R "$src" "$dest"
find "$dest" -type f \( -name '*.sh' -o -name '*.py' \) -exec chmod +x {} + 2>/dev/null || true
rm -f "$dest/$receipt_name"
write_file_atomically "$dest/$receipt_name" "$skill"
printf 'copied %-12s %s\n' "$skill" "$dest"
}
if [ -z "${agents# }" ] && [ -z "$explicit_dirs" ] && [ "$action" = install ]; then
select_agents_interactively
fi
require_bundled_skills
dest_file=$(mktemp)
trap 'rm -f "$dest_file" ${rcpt_tmp:+"$rcpt_tmp"}' EXIT HUP INT TERM
destinations > "$dest_file"
if [ ! -s "$dest_file" ]; then
echo "error: nothing to install; pass --agent or --dir" >&2
exit 1
fi
while IFS="$(printf '\t')" read -r label dir; do
[ -n "$label" ] || continue
[ -n "$dir" ] || continue
for skill in $(selected_skills); do
install_one "$skill" "$dir"
done
done < "$dest_file"
if [ "$action" = install ]; then
echo "done. restart your agent so it rescans skill directories."
fi