skillpull 0.3.6 → 0.4.2
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 +1 -1
- package/skillpull +202 -18
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
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.2"
|
|
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"
|
|
@@ -788,7 +951,11 @@ cmd_alias() {
|
|
|
788
951
|
}
|
|
789
952
|
|
|
790
953
|
cmd_search() {
|
|
791
|
-
local keyword="$1"
|
|
954
|
+
local keyword="${1:-}"
|
|
955
|
+
if [[ -z "$keyword" && -t 0 ]]; then
|
|
956
|
+
printf " Search keyword: "
|
|
957
|
+
read -r keyword
|
|
958
|
+
fi
|
|
792
959
|
[[ -z "$keyword" ]] && { err "Usage: skillpull search <keyword>"; return 1; }
|
|
793
960
|
|
|
794
961
|
dim "Searching GitHub for '$keyword' ..."
|
|
@@ -907,10 +1074,11 @@ main() {
|
|
|
907
1074
|
fi
|
|
908
1075
|
echo ""
|
|
909
1076
|
info "Run 'skillpull init' to set up your default skill repo."
|
|
910
|
-
|
|
911
|
-
usage
|
|
1077
|
+
exit 0
|
|
912
1078
|
fi
|
|
913
|
-
|
|
1079
|
+
|
|
1080
|
+
# Already installed, no args -> treat as pull (will use registry + interactive select)
|
|
1081
|
+
cmd="pull"
|
|
914
1082
|
fi
|
|
915
1083
|
|
|
916
1084
|
while [[ $# -gt 0 ]]; do
|
|
@@ -973,7 +1141,16 @@ main() {
|
|
|
973
1141
|
|
|
974
1142
|
case "${cmd:-}" in
|
|
975
1143
|
pull|"")
|
|
976
|
-
[[ -z "$repo_url" ]]
|
|
1144
|
+
if [[ -z "$repo_url" ]]; then
|
|
1145
|
+
# No source given, try registry
|
|
1146
|
+
local reg; reg="$(read_config_key "registry")"
|
|
1147
|
+
if [[ -z "$reg" ]]; then
|
|
1148
|
+
err "No source specified and no registry set."
|
|
1149
|
+
err "Run 'skillpull init' to set a default skill repo."
|
|
1150
|
+
exit 1
|
|
1151
|
+
fi
|
|
1152
|
+
repo_url="$reg"
|
|
1153
|
+
fi
|
|
977
1154
|
local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
|
|
978
1155
|
# If resolve_repo_url returned registry URL for bare name, use original as skill filter
|
|
979
1156
|
if [[ "$resolved" != "$repo_url" && "$repo_url" != @* && "$repo_url" != */* && "$repo_url" != *"://"* && "$repo_url" != git@* ]]; then
|
|
@@ -987,7 +1164,15 @@ main() {
|
|
|
987
1164
|
done
|
|
988
1165
|
;;
|
|
989
1166
|
list)
|
|
990
|
-
[[ -z "$repo_url" ]]
|
|
1167
|
+
if [[ -z "$repo_url" ]]; then
|
|
1168
|
+
local reg; reg="$(read_config_key "registry")"
|
|
1169
|
+
if [[ -z "$reg" ]]; then
|
|
1170
|
+
err "No source specified and no registry set."
|
|
1171
|
+
err "Run 'skillpull init' to set a default skill repo."
|
|
1172
|
+
exit 1
|
|
1173
|
+
fi
|
|
1174
|
+
repo_url="$reg"
|
|
1175
|
+
fi
|
|
991
1176
|
local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
|
|
992
1177
|
cmd_list "$resolved"
|
|
993
1178
|
;;
|
|
@@ -1025,7 +1210,6 @@ main() {
|
|
|
1025
1210
|
done
|
|
1026
1211
|
;;
|
|
1027
1212
|
remove)
|
|
1028
|
-
[[ -z "$skill_name" ]] && { err "Missing skill name"; usage; exit 1; }
|
|
1029
1213
|
for agent in "${agents[@]}"; do
|
|
1030
1214
|
local target_dir
|
|
1031
1215
|
target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
|