skillpull 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +5 -1
- package/skillpull +217 -32
package/README.md
CHANGED
|
@@ -115,6 +115,12 @@ description: What this skill does
|
|
|
115
115
|
Skill content here...
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
- macOS, Linux, or Windows (via [WSL](https://learn.microsoft.com/en-us/windows/wsl/) or [Git Bash](https://gitforwindows.org/))
|
|
121
|
+
- Git
|
|
122
|
+
- Bash 3+
|
|
123
|
+
|
|
118
124
|
## License
|
|
119
125
|
|
|
120
126
|
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillpull",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Sync AI agent skills from Git repositories to Claude, Codex, Kiro, and Cursor",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skillpull": "./skillpull"
|
|
7
7
|
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "echo '✓ skillpull installed. Run: skillpull init'",
|
|
10
|
+
"preuninstall": "echo '✓ skillpull uninstalled.'"
|
|
11
|
+
},
|
|
8
12
|
"keywords": [
|
|
9
13
|
"ai",
|
|
10
14
|
"skills",
|
package/skillpull
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
VERSION="0.
|
|
4
|
+
VERSION="0.4.0"
|
|
5
5
|
MANIFEST_FILE=".skillpull.json"
|
|
6
6
|
TMPDIR_PREFIX="skillpull"
|
|
7
7
|
CONFIG_DIR="$HOME/.config/skillpull"
|
|
@@ -46,6 +46,118 @@ warn() { printf " ${YELLOW}!${RESET} %s\n" "$*" >&2; }
|
|
|
46
46
|
err() { printf " ${RED}✗${RESET} %s\n" "$*" >&2; }
|
|
47
47
|
dim() { [[ "${QUIET:-0}" == "1" ]] || printf " ${DIM}%s${RESET}\n" "$*"; }
|
|
48
48
|
|
|
49
|
+
# ── Cross-platform sed -i ──
|
|
50
|
+
sedi() {
|
|
51
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
52
|
+
sed -i '' "$@"
|
|
53
|
+
else
|
|
54
|
+
sed -i "$@"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ── Interactive multi-select menu (bash 3 compatible) ──
|
|
59
|
+
# Usage: select_menu "prompt" item1 item2 item3 ...
|
|
60
|
+
# Sets SELECTED_ITEMS=("item1" "item3") with user's choices
|
|
61
|
+
SELECTED_ITEMS=()
|
|
62
|
+
|
|
63
|
+
select_menu() {
|
|
64
|
+
local prompt="$1"; shift
|
|
65
|
+
local items=("$@")
|
|
66
|
+
local count=${#items[@]}
|
|
67
|
+
local cursor=0
|
|
68
|
+
local i
|
|
69
|
+
|
|
70
|
+
# Track selected state with a simple string: "0" = unselected, "1" = selected
|
|
71
|
+
local selected=""
|
|
72
|
+
for ((i=0; i<count; i++)); do
|
|
73
|
+
selected="${selected}0"
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
SELECTED_ITEMS=()
|
|
77
|
+
|
|
78
|
+
# Save terminal state
|
|
79
|
+
local old_stty; old_stty="$(stty -g 2>/dev/null)" || true
|
|
80
|
+
|
|
81
|
+
# Hide cursor
|
|
82
|
+
printf '\033[?25l' >&2
|
|
83
|
+
|
|
84
|
+
# Print prompt
|
|
85
|
+
printf "\n ${CYAN}%s${RESET}\n" "$prompt" >&2
|
|
86
|
+
printf " ${DIM}↑↓ move Space select Enter confirm${RESET}\n\n" >&2
|
|
87
|
+
|
|
88
|
+
# Draw initial list
|
|
89
|
+
for ((i=0; i<count; i++)); do
|
|
90
|
+
local marker="[ ]"
|
|
91
|
+
if [[ "${selected:$i:1}" == "1" ]]; then marker="[x]"; fi
|
|
92
|
+
if [[ $i -eq $cursor ]]; then
|
|
93
|
+
printf " ${CYAN}> %s %s${RESET}\n" "$marker" "${items[$i]}" >&2
|
|
94
|
+
else
|
|
95
|
+
printf " %s %s\n" "$marker" "${items[$i]}" >&2
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
# Input loop
|
|
100
|
+
while true; do
|
|
101
|
+
# Read single key
|
|
102
|
+
local key=""
|
|
103
|
+
IFS= read -rsn1 key 2>/dev/null || true
|
|
104
|
+
|
|
105
|
+
# Handle escape sequences (arrow keys)
|
|
106
|
+
if [[ "$key" == $'\033' ]]; then
|
|
107
|
+
local seq1="" seq2=""
|
|
108
|
+
IFS= read -rsn1 -t 0.1 seq1 2>/dev/null || true
|
|
109
|
+
IFS= read -rsn1 -t 0.1 seq2 2>/dev/null || true
|
|
110
|
+
key="${key}${seq1}${seq2}"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
case "$key" in
|
|
114
|
+
$'\033[A'|k) # Up
|
|
115
|
+
[[ $cursor -gt 0 ]] && cursor=$((cursor - 1))
|
|
116
|
+
;;
|
|
117
|
+
$'\033[B'|j) # Down
|
|
118
|
+
[[ $cursor -lt $((count - 1)) ]] && cursor=$((cursor + 1))
|
|
119
|
+
;;
|
|
120
|
+
' ') # Space - toggle selection
|
|
121
|
+
if [[ "${selected:$cursor:1}" == "0" ]]; then
|
|
122
|
+
selected="${selected:0:$cursor}1${selected:$((cursor+1))}"
|
|
123
|
+
else
|
|
124
|
+
selected="${selected:0:$cursor}0${selected:$((cursor+1))}"
|
|
125
|
+
fi
|
|
126
|
+
;;
|
|
127
|
+
'') # Enter - confirm
|
|
128
|
+
break
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
# Redraw: move cursor up by $count lines
|
|
133
|
+
printf '\033[%dA' "$count" >&2
|
|
134
|
+
|
|
135
|
+
for ((i=0; i<count; i++)); do
|
|
136
|
+
printf '\033[2K' >&2 # Clear line
|
|
137
|
+
local marker="[ ]"
|
|
138
|
+
if [[ "${selected:$i:1}" == "1" ]]; then marker="[x]"; fi
|
|
139
|
+
if [[ $i -eq $cursor ]]; then
|
|
140
|
+
printf " ${CYAN}> %s %s${RESET}\n" "$marker" "${items[$i]}" >&2
|
|
141
|
+
else
|
|
142
|
+
printf " %s %s\n" "$marker" "${items[$i]}" >&2
|
|
143
|
+
fi
|
|
144
|
+
done
|
|
145
|
+
done
|
|
146
|
+
|
|
147
|
+
# Show cursor
|
|
148
|
+
printf '\033[?25h' >&2
|
|
149
|
+
|
|
150
|
+
# Restore terminal
|
|
151
|
+
[[ -n "$old_stty" ]] && stty "$old_stty" 2>/dev/null || true
|
|
152
|
+
|
|
153
|
+
# Collect selected items
|
|
154
|
+
for ((i=0; i<count; i++)); do
|
|
155
|
+
if [[ "${selected:$i:1}" == "1" ]]; then
|
|
156
|
+
SELECTED_ITEMS+=("${items[$i]}")
|
|
157
|
+
fi
|
|
158
|
+
done
|
|
159
|
+
}
|
|
160
|
+
|
|
49
161
|
# ── Cleanup ──
|
|
50
162
|
_TMPDIR=""
|
|
51
163
|
cleanup() { [[ -n "${_TMPDIR:-}" && -d "${_TMPDIR:-}" ]] && rm -rf "$_TMPDIR" || true; }
|
|
@@ -83,12 +195,12 @@ write_alias() {
|
|
|
83
195
|
ensure_config
|
|
84
196
|
local esc_url; esc_url="$(_json_escape "$url")"
|
|
85
197
|
if grep -q "\"${name}\"" "$CONFIG_FILE" 2>/dev/null; then
|
|
86
|
-
|
|
198
|
+
sedi "s|\"${name}\":\"[^\"]*\"|\"${name}\":\"${esc_url}\"|" "$CONFIG_FILE"
|
|
87
199
|
else
|
|
88
200
|
# Insert into aliases object
|
|
89
|
-
|
|
201
|
+
sedi "s|\"aliases\":{|\"aliases\":{\"${name}\":\"${esc_url}\",|" "$CONFIG_FILE"
|
|
90
202
|
# Clean trailing comma before }
|
|
91
|
-
|
|
203
|
+
sedi 's/,}/}/g' "$CONFIG_FILE"
|
|
92
204
|
fi
|
|
93
205
|
}
|
|
94
206
|
|
|
@@ -100,9 +212,9 @@ remove_alias() {
|
|
|
100
212
|
return 1
|
|
101
213
|
fi
|
|
102
214
|
# Remove the alias entry
|
|
103
|
-
|
|
215
|
+
sedi "s|\"${name}\":\"[^\"]*\",\?||" "$CONFIG_FILE"
|
|
104
216
|
# Clean up double commas and trailing commas
|
|
105
|
-
|
|
217
|
+
sedi 's/,,/,/g; s/,}/}/g; s/{,/{/g' "$CONFIG_FILE"
|
|
106
218
|
info "Alias '$name' removed"
|
|
107
219
|
}
|
|
108
220
|
|
|
@@ -131,7 +243,7 @@ set_registry() {
|
|
|
131
243
|
local url="$1"
|
|
132
244
|
ensure_config
|
|
133
245
|
local esc_url; esc_url="$(_json_escape "$url")"
|
|
134
|
-
|
|
246
|
+
sedi "s|\"registry\":\"[^\"]*\"|\"registry\":\"${esc_url}\"|" "$CONFIG_FILE"
|
|
135
247
|
info "Default registry set to: $url"
|
|
136
248
|
}
|
|
137
249
|
|
|
@@ -292,11 +404,11 @@ write_manifest_entry() {
|
|
|
292
404
|
mv "$tmp" "$manifest"
|
|
293
405
|
else
|
|
294
406
|
# Append before closing braces
|
|
295
|
-
|
|
407
|
+
sedi '/"skills":/,$ {
|
|
296
408
|
/^ }/ i\'"$entry"',
|
|
297
409
|
}' "$manifest"
|
|
298
410
|
# Fix trailing comma before closing brace
|
|
299
|
-
|
|
411
|
+
sedi ':a;N;$!ba;s/,\n }/\n }/g' "$manifest"
|
|
300
412
|
fi
|
|
301
413
|
}
|
|
302
414
|
|
|
@@ -367,6 +479,28 @@ cmd_pull() {
|
|
|
367
479
|
return 1
|
|
368
480
|
fi
|
|
369
481
|
skills=("${matched[@]}")
|
|
482
|
+
elif [[ ${#skills[@]} -gt 1 && -t 0 ]]; then
|
|
483
|
+
# Multiple skills, no filter, interactive terminal -> let user choose
|
|
484
|
+
local names=()
|
|
485
|
+
for sd in "${skills[@]}"; do
|
|
486
|
+
names+=("$(skill_display_name "$sd")")
|
|
487
|
+
done
|
|
488
|
+
select_menu "Select skills to install:" "${names[@]}"
|
|
489
|
+
if [[ ${#SELECTED_ITEMS[@]} -eq 0 ]]; then
|
|
490
|
+
warn "No skills selected"
|
|
491
|
+
return 0
|
|
492
|
+
fi
|
|
493
|
+
local matched=()
|
|
494
|
+
for sd in "${skills[@]}"; do
|
|
495
|
+
local sn; sn="$(skill_display_name "$sd")"
|
|
496
|
+
for sel in "${SELECTED_ITEMS[@]}"; do
|
|
497
|
+
if [[ "$sn" == "$sel" ]]; then
|
|
498
|
+
matched+=("$sd")
|
|
499
|
+
break
|
|
500
|
+
fi
|
|
501
|
+
done
|
|
502
|
+
done
|
|
503
|
+
skills=("${matched[@]}")
|
|
370
504
|
fi
|
|
371
505
|
|
|
372
506
|
mkdir -p "$target_dir"
|
|
@@ -491,9 +625,38 @@ cmd_installed() {
|
|
|
491
625
|
}
|
|
492
626
|
|
|
493
627
|
cmd_remove() {
|
|
494
|
-
local skill_name="$1" target_dir="$2" agent="${3:-claude}"
|
|
628
|
+
local skill_name="${1:-}" target_dir="$2" agent="${3:-claude}"
|
|
495
629
|
local fmt; fmt="$(agent_format "$agent")"
|
|
496
630
|
|
|
631
|
+
# No skill name provided -> interactive select
|
|
632
|
+
if [[ -z "$skill_name" && -t 0 ]]; then
|
|
633
|
+
if [[ ! -d "$target_dir" ]]; then
|
|
634
|
+
warn "No skills directory found at $target_dir"
|
|
635
|
+
return 0
|
|
636
|
+
fi
|
|
637
|
+
local installed_skills=()
|
|
638
|
+
local installed_names=()
|
|
639
|
+
while IFS= read -r d; do
|
|
640
|
+
[[ -n "$d" ]] && installed_skills+=("$d") && installed_names+=("$(skill_display_name "$d")")
|
|
641
|
+
done < <(discover_skills "$target_dir")
|
|
642
|
+
|
|
643
|
+
if [[ ${#installed_skills[@]} -eq 0 ]]; then
|
|
644
|
+
warn "No skills installed in $target_dir"
|
|
645
|
+
return 0
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
select_menu "Select skills to remove:" "${installed_names[@]}"
|
|
649
|
+
if [[ ${#SELECTED_ITEMS[@]} -eq 0 ]]; then
|
|
650
|
+
warn "No skills selected"
|
|
651
|
+
return 0
|
|
652
|
+
fi
|
|
653
|
+
|
|
654
|
+
for sel in "${SELECTED_ITEMS[@]}"; do
|
|
655
|
+
cmd_remove "$sel" "$target_dir" "$agent"
|
|
656
|
+
done
|
|
657
|
+
return 0
|
|
658
|
+
fi
|
|
659
|
+
|
|
497
660
|
if [[ "$fmt" == "mdc" ]]; then
|
|
498
661
|
local dest="$target_dir/${skill_name}.mdc"
|
|
499
662
|
if [[ ! -f "$dest" ]]; then
|
|
@@ -527,7 +690,7 @@ cmd_remove() {
|
|
|
527
690
|
local tmp; tmp="$(mktemp)"
|
|
528
691
|
grep -v "\"${skill_name}\"" "$manifest" > "$tmp" || true
|
|
529
692
|
mv "$tmp" "$manifest"
|
|
530
|
-
|
|
693
|
+
sedi ':a;N;$!ba;s/,\n }/\n }/g' "$manifest"
|
|
531
694
|
fi
|
|
532
695
|
|
|
533
696
|
info "Removed: $skill_name from $target_dir"
|
|
@@ -672,13 +835,32 @@ cmd_push() {
|
|
|
672
835
|
cmd_uninstall() {
|
|
673
836
|
local self; self="$(realpath "$0" 2>/dev/null || echo "$0")"
|
|
674
837
|
local local_bin="${SKILLPULL_INSTALL_DIR:-$HOME/.local/bin}/skillpull"
|
|
838
|
+
local removed=0
|
|
675
839
|
|
|
676
840
|
printf "\n ${CYAN}Uninstall skillpull${RESET}\n\n"
|
|
677
841
|
|
|
678
|
-
# Remove local binary
|
|
842
|
+
# Remove local binary (~/.local/bin)
|
|
679
843
|
if [[ -f "$local_bin" ]]; then
|
|
680
844
|
rm -f "$local_bin"
|
|
681
845
|
info "Removed $local_bin"
|
|
846
|
+
removed=1
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
# Remove npm global package (this is likely where the command lives)
|
|
850
|
+
if command -v npm &>/dev/null; then
|
|
851
|
+
local npm_pkg; npm_pkg="$(npm ls -g skillpull --parseable 2>/dev/null)" || true
|
|
852
|
+
if [[ -n "$npm_pkg" ]]; then
|
|
853
|
+
npm uninstall -g skillpull 2>/dev/null
|
|
854
|
+
info "Removed npm global package"
|
|
855
|
+
removed=1
|
|
856
|
+
fi
|
|
857
|
+
fi
|
|
858
|
+
|
|
859
|
+
# Remove the running binary itself if still exists and wasn't already cleaned
|
|
860
|
+
if [[ -f "$self" && "$self" != "$local_bin" ]]; then
|
|
861
|
+
rm -f "$self" 2>/dev/null || true
|
|
862
|
+
info "Removed $self"
|
|
863
|
+
removed=1
|
|
682
864
|
fi
|
|
683
865
|
|
|
684
866
|
# Remove config
|
|
@@ -693,21 +875,12 @@ cmd_uninstall() {
|
|
|
693
875
|
fi
|
|
694
876
|
fi
|
|
695
877
|
|
|
696
|
-
# npm global uninstall
|
|
697
|
-
if command -v npm &>/dev/null; then
|
|
698
|
-
local npm_global; npm_global="$(npm root -g 2>/dev/null)/../bin/skillpull"
|
|
699
|
-
if [[ -f "$npm_global" ]] || npm ls -g skillpull &>/dev/null 2>&1; then
|
|
700
|
-
printf " Remove npm global package? (y/n) "
|
|
701
|
-
read -r yn
|
|
702
|
-
if [[ "$yn" == "y" || "$yn" == "Y" ]]; then
|
|
703
|
-
npm uninstall -g skillpull 2>/dev/null
|
|
704
|
-
info "npm global package removed"
|
|
705
|
-
fi
|
|
706
|
-
fi
|
|
707
|
-
fi
|
|
708
|
-
|
|
709
878
|
echo ""
|
|
710
|
-
|
|
879
|
+
if [[ "$removed" == "1" ]]; then
|
|
880
|
+
info "skillpull uninstalled."
|
|
881
|
+
else
|
|
882
|
+
warn "skillpull not found. Already uninstalled?"
|
|
883
|
+
fi
|
|
711
884
|
}
|
|
712
885
|
|
|
713
886
|
cmd_init() {
|
|
@@ -887,17 +1060,21 @@ main() {
|
|
|
887
1060
|
local global_bin="${SKILLPULL_INSTALL_DIR:-$HOME/.local/bin}/skillpull"
|
|
888
1061
|
|
|
889
1062
|
if [[ "$self" != "$global_bin" && ! -f "$global_bin" ]]; then
|
|
1063
|
+
info "Installing skillpull v${VERSION} ..."
|
|
890
1064
|
mkdir -p "$(dirname "$global_bin")"
|
|
891
1065
|
cp "$self" "$global_bin"
|
|
892
1066
|
chmod +x "$global_bin"
|
|
893
|
-
info "Installed
|
|
1067
|
+
info "Installed to $global_bin"
|
|
894
1068
|
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$(dirname "$global_bin")"; then
|
|
895
1069
|
warn "Add to PATH: export PATH=\"$(dirname "$global_bin"):\$PATH\""
|
|
896
1070
|
fi
|
|
897
|
-
|
|
898
|
-
|
|
1071
|
+
echo ""
|
|
1072
|
+
info "Run 'skillpull init' to set up your default skill repo."
|
|
1073
|
+
exit 0
|
|
899
1074
|
fi
|
|
900
|
-
|
|
1075
|
+
|
|
1076
|
+
# Already installed, no args -> treat as pull (will use registry + interactive select)
|
|
1077
|
+
cmd="pull"
|
|
901
1078
|
fi
|
|
902
1079
|
|
|
903
1080
|
while [[ $# -gt 0 ]]; do
|
|
@@ -960,7 +1137,16 @@ main() {
|
|
|
960
1137
|
|
|
961
1138
|
case "${cmd:-}" in
|
|
962
1139
|
pull|"")
|
|
963
|
-
[[ -z "$repo_url" ]]
|
|
1140
|
+
if [[ -z "$repo_url" ]]; then
|
|
1141
|
+
# No source given, try registry
|
|
1142
|
+
local reg; reg="$(read_config_key "registry")"
|
|
1143
|
+
if [[ -z "$reg" ]]; then
|
|
1144
|
+
err "No source specified and no registry set."
|
|
1145
|
+
err "Run 'skillpull init' to set a default skill repo."
|
|
1146
|
+
exit 1
|
|
1147
|
+
fi
|
|
1148
|
+
repo_url="$reg"
|
|
1149
|
+
fi
|
|
964
1150
|
local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
|
|
965
1151
|
# If resolve_repo_url returned registry URL for bare name, use original as skill filter
|
|
966
1152
|
if [[ "$resolved" != "$repo_url" && "$repo_url" != @* && "$repo_url" != */* && "$repo_url" != *"://"* && "$repo_url" != git@* ]]; then
|
|
@@ -1012,7 +1198,6 @@ main() {
|
|
|
1012
1198
|
done
|
|
1013
1199
|
;;
|
|
1014
1200
|
remove)
|
|
1015
|
-
[[ -z "$skill_name" ]] && { err "Missing skill name"; usage; exit 1; }
|
|
1016
1201
|
for agent in "${agents[@]}"; do
|
|
1017
1202
|
local target_dir
|
|
1018
1203
|
target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
|