feat(skills): make Codex the default install target
This commit is contained in:
+58
-16
@@ -1,13 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-skills.sh — Cross-machine Claude skill sync from ai-proj-helper
|
||||
# install-skills.sh — Cross-agent skill sync from ai-proj-helper
|
||||
#
|
||||
# Usage:
|
||||
# ./install-skills.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --agent <agent> Install target: codex (default) or claude
|
||||
# --dry-run Preview changes without writing anything
|
||||
# --category <cat> Only install plugins in dir_category=<cat>
|
||||
# Valid values: biz, core, dev, integration, personal, req
|
||||
# --exclude <name> Skip one install_name (repeatable)
|
||||
# --force Overwrite even if local files were modified
|
||||
# --cleanup Remove locally installed skills that are no longer in repo
|
||||
# --list List all available plugins without installing
|
||||
@@ -16,12 +18,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILLS_DIR="${HOME}/.claude/skills"
|
||||
COMMANDS_DIR="${HOME}/.claude/commands"
|
||||
STATE_FILE="${HOME}/.claude/.installed-skills.json"
|
||||
SKILLS_DIR=""
|
||||
COMMANDS_DIR=""
|
||||
STATE_FILE=""
|
||||
|
||||
AGENT_TARGET="codex"
|
||||
DRY_RUN=false
|
||||
CATEGORY_FILTER=""
|
||||
EXCLUDED_NAMES=()
|
||||
FORCE=false
|
||||
CLEANUP=false
|
||||
LIST_ONLY=false
|
||||
@@ -43,11 +47,19 @@ dry() { echo -e "${YELLOW}[dry]${RESET} $*"; }
|
||||
# ── Argument parsing ───────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--agent)
|
||||
[[ $# -ge 2 ]] || { error "--agent requires codex or claude"; exit 1; }
|
||||
AGENT_TARGET="$2"; shift ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--force) FORCE=true ;;
|
||||
--cleanup) CLEANUP=true ;;
|
||||
--list) LIST_ONLY=true ;;
|
||||
--category) CATEGORY_FILTER="$2"; shift ;;
|
||||
--category)
|
||||
[[ $# -ge 2 ]] || { error "--category requires a value"; exit 1; }
|
||||
CATEGORY_FILTER="$2"; shift ;;
|
||||
--exclude)
|
||||
[[ $# -ge 2 ]] || { error "--exclude requires an install_name"; exit 1; }
|
||||
EXCLUDED_NAMES+=("$2"); shift ;;
|
||||
--help|-h)
|
||||
grep '^#' "$0" | grep -v '!/usr' | sed 's/^# \?//'
|
||||
exit 0 ;;
|
||||
@@ -58,6 +70,24 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
done
|
||||
|
||||
case "$AGENT_TARGET" in
|
||||
codex)
|
||||
# ~/.agents/skills is the current user-level Codex discovery location and
|
||||
# is intentionally agent-neutral. Commands are installed as normal skills.
|
||||
SKILLS_DIR="${AI_PROJ_HELPER_SKILLS_DIR:-${HOME}/.agents/skills}"
|
||||
STATE_FILE="${AI_PROJ_HELPER_STATE_FILE:-${HOME}/.agents/.ai-proj-helper-installed-skills.json}"
|
||||
;;
|
||||
claude)
|
||||
SKILLS_DIR="${AI_PROJ_HELPER_SKILLS_DIR:-${HOME}/.claude/skills}"
|
||||
COMMANDS_DIR="${AI_PROJ_HELPER_COMMANDS_DIR:-${HOME}/.claude/commands}"
|
||||
STATE_FILE="${AI_PROJ_HELPER_STATE_FILE:-${HOME}/.claude/.installed-skills.json}"
|
||||
;;
|
||||
*)
|
||||
error "Unsupported agent: $AGENT_TARGET (expected codex or claude)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── State helpers (plain JSON via python3) ─────────────────────────────────────
|
||||
state_get() {
|
||||
# state_get <install_name> -> prints version or empty string
|
||||
@@ -94,7 +124,7 @@ state_set() {
|
||||
import json,os
|
||||
f='$STATE_FILE'
|
||||
d=json.load(open(f)) if os.path.exists(f) else {}
|
||||
d['$name']={'version':'$ver','install_type':'$itype','content_digest':'$digest'}
|
||||
d['$name']={'version':'$ver','install_type':'$itype','content_digest':'$digest','agent':'$AGENT_TARGET'}
|
||||
json.dump(d,open(f,'w'),indent=2)
|
||||
" 2>/dev/null
|
||||
}
|
||||
@@ -149,7 +179,9 @@ files = [target] if target.is_file() else sorted(
|
||||
path for path in target.rglob("*") if path.is_file() or path.is_symlink()
|
||||
)
|
||||
for path in files:
|
||||
relative = path.name if target.is_file() else path.relative_to(target).as_posix()
|
||||
# A single-file command is renamed when installed for Claude. Hash its
|
||||
# content under a stable logical name so source and target compare equally.
|
||||
relative = "." if target.is_file() else path.relative_to(target).as_posix()
|
||||
digest.update(relative.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
if path.is_symlink():
|
||||
@@ -222,6 +254,11 @@ install_plugin() {
|
||||
dir_category="$(read_field "$json_path" dir_category)"
|
||||
version="$(read_field "$json_path" version)"
|
||||
|
||||
local excluded
|
||||
for excluded in ${EXCLUDED_NAMES[@]+"${EXCLUDED_NAMES[@]}"}; do
|
||||
[[ "$install_name" == "$excluded" ]] && return
|
||||
done
|
||||
|
||||
# Skip if no install metadata (legacy plugin without our new fields)
|
||||
if [[ -z "$install_name" || -z "$install_type" ]]; then
|
||||
warn "$(basename "$plugin_dir"): missing install_name/install_type, skipping"
|
||||
@@ -239,8 +276,13 @@ install_plugin() {
|
||||
return
|
||||
fi
|
||||
|
||||
local effective_install_type="$install_type"
|
||||
if [[ "$AGENT_TARGET" == "codex" ]]; then
|
||||
effective_install_type="skill"
|
||||
fi
|
||||
|
||||
if [[ "$LIST_ONLY" == true ]]; then
|
||||
echo " [$dir_category] $install_type:$install_name v$version"
|
||||
echo " [$dir_category] $effective_install_type:$install_name v$version"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -250,7 +292,7 @@ install_plugin() {
|
||||
src_dir="$(resolve_skills_src "$skills_dir")"
|
||||
|
||||
local source_path target_path
|
||||
if [[ "$install_type" == "command" ]]; then
|
||||
if [[ "$effective_install_type" == "command" ]]; then
|
||||
source_path="$src_dir/SKILL.md"
|
||||
target_path="$COMMANDS_DIR/${install_name}.md"
|
||||
else
|
||||
@@ -274,13 +316,13 @@ install_plugin() {
|
||||
|
||||
if [[ "$FORCE" == false && -n "$target_digest" && "$target_digest" == "$source_digest" ]]; then
|
||||
if [[ "$DRY_RUN" == false && ( "$current_version" != "$version" || "$recorded_digest" != "$source_digest" ) ]]; then
|
||||
state_set "$install_name" "$version" "$install_type" "$source_digest"
|
||||
state_set "$install_name" "$version" "$effective_install_type" "$source_digest"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
local legacy_subset=false
|
||||
if [[ "$install_type" == "skill" && -z "$recorded_digest" && -n "$target_digest" ]]; then
|
||||
if [[ "$effective_install_type" == "skill" && -z "$recorded_digest" && -n "$target_digest" ]]; then
|
||||
if is_compatible_subset "$source_path" "$target_path"; then
|
||||
legacy_subset=true
|
||||
fi
|
||||
@@ -294,7 +336,7 @@ install_plugin() {
|
||||
fi
|
||||
|
||||
# Perform install
|
||||
if [[ "$install_type" == "command" ]]; then
|
||||
if [[ "$effective_install_type" == "command" ]]; then
|
||||
# Single-file command → ~/.claude/commands/<name>.md
|
||||
local src_md="$src_dir/SKILL.md"
|
||||
if [[ ! -f "$src_md" ]]; then
|
||||
@@ -308,13 +350,13 @@ install_plugin() {
|
||||
else
|
||||
mkdir -p "$COMMANDS_DIR"
|
||||
cp "$src_md" "$COMMANDS_DIR/${install_name}.md"
|
||||
state_set "$install_name" "$version" "$install_type" "$source_digest"
|
||||
state_set "$install_name" "$version" "$effective_install_type" "$source_digest"
|
||||
ok "$install_name → command (v$version)"
|
||||
INSTALL_ACTION=true
|
||||
fi
|
||||
|
||||
else
|
||||
# Skill directory → ~/.claude/skills/<name>/
|
||||
# Standard skill directory → the selected agent's discovery root.
|
||||
local dst_dir="$SKILLS_DIR/$install_name"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
@@ -324,7 +366,7 @@ install_plugin() {
|
||||
mkdir -p "$dst_dir"
|
||||
# rsync resolved source (handles nested skills/ structures)
|
||||
rsync -a --delete "$src_dir/" "$dst_dir/"
|
||||
state_set "$install_name" "$version" "$install_type" "$source_digest"
|
||||
state_set "$install_name" "$version" "$effective_install_type" "$source_digest"
|
||||
ok "$install_name → skill (v$version)"
|
||||
INSTALL_ACTION=true
|
||||
fi
|
||||
@@ -387,7 +429,7 @@ main() {
|
||||
return
|
||||
fi
|
||||
|
||||
info "Installing Claude skills from: $REPO_DIR"
|
||||
info "Installing skills for $AGENT_TARGET from: $REPO_DIR"
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be written"
|
||||
[[ -n "$CATEGORY_FILTER" ]] && info "Category filter: $CATEGORY_FILTER"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user