claude-skills/scripts/codex-install.sh
Claude 5e67efdf58
ci: check Codex, Gemini, Hermes and Vibe installs and mirror drift
- Gate G13 (scripts/check_harness_mirrors.py): regenerate the .codex,
  .gemini, .vibe and .hermes mirrors from scratch in a temp copy and fail on
  any difference in link names, link targets or index bytes. --fix rewrites
  them, replacing hand-run syncs.
- harness-compat.yml + scripts/check_harness_installs.py: install the real
  Codex, Gemini CLI, Hermes Agent and Mistral Vibe CLIs (pinned on PRs, latest
  weekly), run the documented install path into an empty HOME and fail unless
  the harness itself lists every installed skill. Offline listings only, no
  model calls or API keys.
- Fix: Codex skipped 6 single-skill plugins because their installed folder
  held .claude-plugin/; the Codex installers now drop it.
- Fix: Gemini skipped 8 command entries without a frontmatter name; added.
- Regenerated 6 Vibe/Hermes links that a fresh sync points at the bundle copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8KYFHtMEP35nSKLy8fRkj
2026-11-03 16:41:48 +00:00

429 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Codex Installation Script for Claude Skills Library
#
# Installs skills from this repository to your local Codex skills directory.
# Follows symlinks to copy actual skill contents.
#
# Usage:
# ./scripts/codex-install.sh (--category <name>[,<name>] | --skill <name> | --all)
#
# Options:
# --category <name> Install one or more categories (comma-separated)
# --skill <name> Install a single skill by name
# --all Install every skill (not recommended, see below)
# --list List available skills and categories
# --dry-run Show what would be installed without making changes
# --help Show this help message
#
# Codex fits all skill descriptions into a fixed prompt budget. Around 100
# skills it starts cutting descriptions short; with the full library (~380)
# it drops them entirely and Codex can no longer tell which skill fits a task.
# Install the categories you use. Run without arguments to see them.
#
# Examples:
# ./scripts/codex-install.sh --category engineering,product
# ./scripts/codex-install.sh --skill content-creator
# ./scripts/codex-install.sh --list
#
set -e
# Configuration
CODEX_SKILLS_DIR="${CODEX_SKILLS_DIR:-$HOME/.codex/skills}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
CODEX_SKILLS_SRC="$REPO_ROOT/.codex/skills"
CODEX_INDEX="$REPO_ROOT/.codex/skills-index.json"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Print colored output
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Show help
show_help() {
sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
# Codex's skill list budget: beyond this many installed skills, descriptions
# are truncated, and with the full library they disappear.
CODEX_SKILL_WARN_AT=100
warn_if_crowded() {
[[ -d $CODEX_SKILLS_DIR ]] || return 0
local total
total=$(find "$CODEX_SKILLS_DIR" -mindepth 2 -maxdepth 2 -name SKILL.md | wc -l | tr -d ' ')
if ((total > CODEX_SKILL_WARN_AT)); then
echo ""
print_warning "$total skills are now installed in $CODEX_SKILLS_DIR."
print_warning "Above about $CODEX_SKILL_WARN_AT, Codex shortens skill descriptions; with the full library it shows names only."
print_warning "Remove categories you don't use from $CODEX_SKILLS_DIR to keep skill selection working."
fi
}
# Shown when no mode is given: installing everything is no longer the default.
choose_hint() {
print_error "Choose what to install: --category <name>[,<name>], --skill <name>, or --all."
echo ""
if [[ -f $CODEX_INDEX ]] && command -v python3 &> /dev/null; then
CODEX_INDEX="$CODEX_INDEX" python3 << 'EOF'
import json, os
cats = json.load(open(os.environ["CODEX_INDEX"], encoding="utf-8")).get("categories", {})
print("Categories:")
for name, info in sorted(cats.items()):
print(f" {name:22} {info['count']:>3} skills")
EOF
fi
echo ""
print_info "Example: $0 --category engineering,product"
print_info "--all installs every skill, which leaves Codex with skill names but no descriptions."
exit 2
}
# Check prerequisites
check_prerequisites() {
if [[ ! -d $CODEX_SKILLS_SRC ]]; then
print_error "Codex skills directory not found: $CODEX_SKILLS_SRC"
print_info "Run 'python scripts/sync-codex-skills.py' first to generate symlinks."
exit 1
fi
if [[ ! -f $CODEX_INDEX ]]; then
print_warning "skills-index.json not found. Some features may be limited."
fi
}
# List available skills
list_skills() {
print_info "Available skills in $CODEX_SKILLS_SRC:"
echo ""
if [[ -f $CODEX_INDEX ]] && command -v python3 &> /dev/null; then
# Use Python to parse JSON and display nicely
CODEX_INDEX="$CODEX_INDEX" python3 << 'EOF'
import json
import os
import sys
try:
with open(os.environ["CODEX_INDEX"], "r", encoding="utf-8") as f:
index = json.load(f)
print("Categories:")
print("-" * 50)
for cat, info in index.get('categories', {}).items():
print(f" {cat}: {info['count']} skills")
print()
print("Skills by category:")
print("-" * 50)
current_cat = None
for skill in index.get('skills', []):
if skill['category'] != current_cat:
current_cat = skill['category']
print(f"\n [{current_cat}]")
print(f" - {skill['name']}: {skill['description'][:60]}...")
except Exception as e:
print(f"Error parsing index: {e}")
sys.exit(1)
EOF
else
# Fallback to simple listing
for skill in "$CODEX_SKILLS_SRC"/*; do
if [[ -L $skill ]] && [[ -e "$skill/SKILL.md" ]]; then
echo " - $(basename "$skill")"
fi
done
fi
exit 0
}
# Install a single skill
install_skill() {
local skill_name="$1"
local dry_run="$2"
if [[ -z $skill_name || $skill_name == */* || $skill_name == *..* ]]; then
print_error "Invalid skill name: $skill_name"
return 1
fi
local skill_src="$CODEX_SKILLS_SRC/$skill_name"
local skill_dest="$CODEX_SKILLS_DIR/$skill_name"
# Check if skill exists
if [[ ! -e $skill_src ]]; then
print_error "Skill not found: $skill_name"
return 1
fi
# Check if it's a valid skill (has SKILL.md)
if [[ ! -e "$skill_src/SKILL.md" ]]; then
print_error "Invalid skill (no SKILL.md): $skill_name"
return 1
fi
if [[ $dry_run == "true" ]]; then
print_info "[DRY RUN] Would install: $skill_name -> $skill_dest"
return 0
fi
# Create destination directory
mkdir -p "$CODEX_SKILLS_DIR"
# Remove existing installation
if [[ -e $skill_dest ]]; then
print_info "Updating existing skill: $skill_name"
rm -rf "$skill_dest"
fi
# Copy skill (following symlinks with -L)
cp -rL "$skill_src" "$skill_dest"
# Codex scans installed skill folders recursively, so a fixture such as
# skill-tester/assets/sample-skill/SKILL.md would load as a real skill.
find "$skill_dest" -mindepth 2 -path '*/assets/*' -name SKILL.md \
-exec sh -c 'mv "$1" "$1.fixture"' _ {} \;
# A single-skill plugin keeps its Claude Code manifest next to SKILL.md.
# Codex skips any skill folder holding a .claude-plugin/ directory (verified
# on codex 0.166), so drop it: the manifest means nothing to Codex.
rm -rf "$skill_dest/.claude-plugin"
print_success "Installed: $skill_name"
return 0
}
# Install skills by category
install_category() {
local category="$1"
local dry_run="$2"
local installed=0
local failed=0
if [[ ! -f $CODEX_INDEX ]]; then
print_error "skills-index.json required for category installation"
exit 1
fi
if ! command -v python3 &> /dev/null; then
print_error "python3 is required for category installation"
exit 1
fi
print_info "Installing skills from category: $category"
# Get skills for this category from index
local skills
skills=$(
CODEX_INDEX="$CODEX_INDEX" CATEGORY="$category" python3 << 'EOF'
import json
import os
with open(os.environ["CODEX_INDEX"], "r", encoding="utf-8") as f:
index = json.load(f)
seen = set()
for skill in index.get('skills', []):
name = skill.get('name')
if skill.get('category') in os.environ["CATEGORY"].split(",") and name and name not in seen:
seen.add(name)
print(name)
EOF
)
if [[ -z $skills ]]; then
print_error "No skills found for category: $category"
exit 1
fi
while IFS= read -r skill; do
if install_skill "$skill" "$dry_run"; then
((++installed))
else
((++failed))
fi
done <<< "$skills"
echo ""
print_info "Category '$category' complete: $installed installed, $failed failed"
[[ $dry_run == "true" ]] || warn_if_crowded
}
# Install all skills
install_all() {
local dry_run="$1"
local installed=0
local failed=0
local skills=""
print_info "Installing all skills to: $CODEX_SKILLS_DIR"
print_warning "With every skill installed, Codex drops skill descriptions from its prompt."
print_warning "Prefer --category <name>[,<name>] for the domains you use."
echo ""
if [[ -f $CODEX_INDEX ]] && command -v python3 &> /dev/null; then
skills=$(
CODEX_INDEX="$CODEX_INDEX" python3 << 'EOF'
import json
import os
with open(os.environ["CODEX_INDEX"], "r", encoding="utf-8") as f:
index = json.load(f)
seen = set()
for skill in index.get("skills", []):
name = skill.get("name")
if name and name not in seen:
seen.add(name)
print(name)
EOF
)
while IFS= read -r skill_name; do
if [[ -z $skill_name ]]; then
continue
fi
if install_skill "$skill_name" "$dry_run"; then
((++installed))
else
((++failed))
fi
done <<< "$skills"
else
for skill in "$CODEX_SKILLS_SRC"/*; do
if [[ -L $skill ]] || [[ -d $skill ]]; then
local skill_name
skill_name=$(basename "$skill")
if install_skill "$skill_name" "$dry_run"; then
((++installed))
else
((++failed))
fi
fi
done
fi
echo ""
print_info "Installation complete: $installed installed, $failed failed"
if [[ $dry_run != "true" ]]; then
echo ""
print_success "Skills installed to: $CODEX_SKILLS_DIR"
print_info "Verify with: ls $CODEX_SKILLS_DIR"
warn_if_crowded
fi
}
# Main
main() {
local mode=""
local target=""
local dry_run="false"
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--all)
mode="all"
shift
;;
--category)
if [[ $# -lt 2 || -z ${2:-} || ${2:-} == --* ]]; then
print_error "Category name required"
exit 1
fi
mode="category"
target="$2"
shift 2
;;
--skill)
if [[ $# -lt 2 || -z ${2:-} || ${2:-} == --* ]]; then
print_error "Skill name required"
exit 1
fi
mode="skill"
target="$2"
shift 2
;;
--list)
mode="list"
shift
;;
--dry-run)
dry_run="true"
shift
;;
--help | -h)
show_help
;;
*)
print_error "Unknown option: $1"
show_help
;;
esac
done
# Banner
echo ""
echo "========================================"
echo " Claude Skills - Codex Installer"
echo "========================================"
echo ""
check_prerequisites
case $mode in
list)
list_skills
;;
skill)
if [[ -z $target ]]; then
print_error "Skill name required"
exit 1
fi
install_skill "$target" "$dry_run"
;;
category)
if [[ -z $target ]]; then
print_error "Category name required"
exit 1
fi
install_category "$target" "$dry_run"
;;
all)
install_all "$dry_run"
;;
"")
choose_hint
;;
esac
}
main "$@"