skillpull 0.3.6 → 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 +1 -1
- package/skillpull +188 -16
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.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"
|
|
@@ -907,10 +1070,11 @@ main() {
|
|
|
907
1070
|
fi
|
|
908
1071
|
echo ""
|
|
909
1072
|
info "Run 'skillpull init' to set up your default skill repo."
|
|
910
|
-
|
|
911
|
-
usage
|
|
1073
|
+
exit 0
|
|
912
1074
|
fi
|
|
913
|
-
|
|
1075
|
+
|
|
1076
|
+
# Already installed, no args -> treat as pull (will use registry + interactive select)
|
|
1077
|
+
cmd="pull"
|
|
914
1078
|
fi
|
|
915
1079
|
|
|
916
1080
|
while [[ $# -gt 0 ]]; do
|
|
@@ -973,7 +1137,16 @@ main() {
|
|
|
973
1137
|
|
|
974
1138
|
case "${cmd:-}" in
|
|
975
1139
|
pull|"")
|
|
976
|
-
[[ -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
|
|
977
1150
|
local resolved; resolved="$(resolve_repo_url "$repo_url")" || exit 1
|
|
978
1151
|
# If resolve_repo_url returned registry URL for bare name, use original as skill filter
|
|
979
1152
|
if [[ "$resolved" != "$repo_url" && "$repo_url" != @* && "$repo_url" != */* && "$repo_url" != *"://"* && "$repo_url" != git@* ]]; then
|
|
@@ -1025,7 +1198,6 @@ main() {
|
|
|
1025
1198
|
done
|
|
1026
1199
|
;;
|
|
1027
1200
|
remove)
|
|
1028
|
-
[[ -z "$skill_name" ]] && { err "Missing skill name"; usage; exit 1; }
|
|
1029
1201
|
for agent in "${agents[@]}"; do
|
|
1030
1202
|
local target_dir
|
|
1031
1203
|
target_dir="$(resolve_target_dir "$agent" "$is_global" "$custom_path")" || continue
|