skill-linker 2.0.0 → 3.0.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 +43 -64
- package/bin/cli.js +3 -43
- package/package.json +11 -5
- package/src/cli.js +45 -0
- package/src/commands/install.js +260 -0
- package/src/commands/list.js +78 -0
- package/src/utils/agents.js +91 -0
- package/src/utils/file-system.js +166 -0
- package/src/utils/git.js +108 -0
- package/link-skill.sh +0 -413
package/link-skill.sh
DELETED
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# link-skill.sh
|
|
4
|
-
# Interactive script to link AI Agent Skills to various AI agents
|
|
5
|
-
DEFAULT_LIB_PATH="$HOME/Documents/AgentSkills"
|
|
6
|
-
SKILL_PATH=""
|
|
7
|
-
FROM_URL=""
|
|
8
|
-
LIST_MODE=false
|
|
9
|
-
|
|
10
|
-
# Helper function for colored output
|
|
11
|
-
print_info() { echo -e "\033[1;34m[INFO]\033[0m $1"; }
|
|
12
|
-
print_success() { echo -e "\033[1;32m[SUCCESS]\033[0m $1"; }
|
|
13
|
-
print_warning() { echo -e "\033[1;33m[WARNING]\033[0m $1"; }
|
|
14
|
-
print_error() { echo -e "\033[1;31m[ERROR]\033[0m $1"; }
|
|
15
|
-
|
|
16
|
-
# Show help
|
|
17
|
-
show_help() {
|
|
18
|
-
echo "Usage: link-skill.sh [OPTIONS] [SKILL_PATH]"
|
|
19
|
-
echo ""
|
|
20
|
-
echo "Options:"
|
|
21
|
-
echo " --from <github_url> Clone skill from GitHub URL first, then link"
|
|
22
|
-
echo " --list List cloned repos and select skills to link"
|
|
23
|
-
echo " --help Show this help message"
|
|
24
|
-
echo ""
|
|
25
|
-
echo "Examples:"
|
|
26
|
-
echo " ./link-skill.sh # Interactive selection from library"
|
|
27
|
-
echo " ./link-skill.sh /path/to/skill # Link specific local skill"
|
|
28
|
-
echo " ./link-skill.sh --from https://github.com/user/my-skill"
|
|
29
|
-
echo " ./link-skill.sh --from https://github.com/anthropics/skills/tree/main/skills/pdf"
|
|
30
|
-
echo ""
|
|
31
|
-
echo "Notes:"
|
|
32
|
-
echo " - If the cloned repo has a 'skills/' subdirectory, you can pick a specific skill"
|
|
33
|
-
echo " - GitHub URLs with /tree/branch/path are supported for direct subpath access"
|
|
34
|
-
echo ""
|
|
35
|
-
exit 0
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# List repos function
|
|
39
|
-
list_repos() {
|
|
40
|
-
if [ ! -d "$DEFAULT_LIB_PATH" ]; then
|
|
41
|
-
print_error "Skill Library not found: $DEFAULT_LIB_PATH"
|
|
42
|
-
print_info "Use --from <github_url> to clone skills first."
|
|
43
|
-
exit 1
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# Find all repos (owner/repo structure)
|
|
47
|
-
repos=()
|
|
48
|
-
repo_paths=()
|
|
49
|
-
for owner_dir in "$DEFAULT_LIB_PATH"/*/; do
|
|
50
|
-
[ -d "$owner_dir" ] || continue
|
|
51
|
-
owner=$(basename "$owner_dir")
|
|
52
|
-
for repo_dir in "$owner_dir"*/; do
|
|
53
|
-
[ -d "$repo_dir" ] || continue
|
|
54
|
-
repo=$(basename "$repo_dir")
|
|
55
|
-
repos+=("$owner/$repo")
|
|
56
|
-
repo_paths+=("$repo_dir")
|
|
57
|
-
done
|
|
58
|
-
done
|
|
59
|
-
|
|
60
|
-
if [ ${#repos[@]} -eq 0 ]; then
|
|
61
|
-
print_warning "No repos found in $DEFAULT_LIB_PATH"
|
|
62
|
-
print_info "Use --from <github_url> to clone skills first."
|
|
63
|
-
exit 0
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
|
-
echo ""
|
|
67
|
-
print_info "Cloned Repos in Skill Library:"
|
|
68
|
-
echo ""
|
|
69
|
-
select repo_name in "${repos[@]}"; do
|
|
70
|
-
if [ -n "$repo_name" ]; then
|
|
71
|
-
idx=$((REPLY - 1))
|
|
72
|
-
selected_repo_path="${repo_paths[$idx]}"
|
|
73
|
-
break
|
|
74
|
-
else
|
|
75
|
-
echo "Invalid selection. Please try again."
|
|
76
|
-
fi
|
|
77
|
-
done
|
|
78
|
-
|
|
79
|
-
# Check for skills/ subdirectory
|
|
80
|
-
SKILLS_DIR="${selected_repo_path}skills"
|
|
81
|
-
if [ -d "$SKILLS_DIR" ]; then
|
|
82
|
-
print_info "Skills in $repo_name:"
|
|
83
|
-
sub_skills=("$SKILLS_DIR"/*/)
|
|
84
|
-
if [ ${#sub_skills[@]} -gt 0 ]; then
|
|
85
|
-
sub_skill_names=()
|
|
86
|
-
for s in "${sub_skills[@]}"; do
|
|
87
|
-
[ -d "$s" ] && sub_skill_names+=("$(basename "$s")")
|
|
88
|
-
done
|
|
89
|
-
|
|
90
|
-
echo ""
|
|
91
|
-
select sub_skill_name in "${sub_skill_names[@]}" "Link entire repo"; do
|
|
92
|
-
if [ -n "$sub_skill_name" ]; then
|
|
93
|
-
if [ "$sub_skill_name" == "Link entire repo" ]; then
|
|
94
|
-
SKILL_PATH="${selected_repo_path%/}"
|
|
95
|
-
else
|
|
96
|
-
SKILL_PATH="$SKILLS_DIR/$sub_skill_name"
|
|
97
|
-
fi
|
|
98
|
-
break
|
|
99
|
-
else
|
|
100
|
-
echo "Invalid selection. Please try again."
|
|
101
|
-
fi
|
|
102
|
-
done
|
|
103
|
-
else
|
|
104
|
-
SKILL_PATH="${selected_repo_path%/}"
|
|
105
|
-
fi
|
|
106
|
-
else
|
|
107
|
-
SKILL_PATH="${selected_repo_path%/}"
|
|
108
|
-
fi
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
# Parse arguments
|
|
112
|
-
while [[ $# -gt 0 ]]; do
|
|
113
|
-
case $1 in
|
|
114
|
-
--from)
|
|
115
|
-
FROM_URL="$2"
|
|
116
|
-
shift 2
|
|
117
|
-
;;
|
|
118
|
-
--list|-l)
|
|
119
|
-
LIST_MODE=true
|
|
120
|
-
shift
|
|
121
|
-
;;
|
|
122
|
-
--help|-h)
|
|
123
|
-
show_help
|
|
124
|
-
;;
|
|
125
|
-
*)
|
|
126
|
-
SKILL_PATH="$1"
|
|
127
|
-
shift
|
|
128
|
-
;;
|
|
129
|
-
esac
|
|
130
|
-
done
|
|
131
|
-
|
|
132
|
-
# Handle --list mode
|
|
133
|
-
if [ "$LIST_MODE" = true ]; then
|
|
134
|
-
list_repos
|
|
135
|
-
fi
|
|
136
|
-
|
|
137
|
-
# 1. Handle --from flag: Clone from GitHub
|
|
138
|
-
if [ -n "$FROM_URL" ]; then
|
|
139
|
-
# Parse GitHub URL - check for /tree/branch/path format
|
|
140
|
-
SUBPATH=""
|
|
141
|
-
CLEAN_URL="$FROM_URL"
|
|
142
|
-
|
|
143
|
-
if [[ "$FROM_URL" =~ (.+)/tree/([^/]+)/(.+)$ ]]; then
|
|
144
|
-
# URL contains /tree/branch/path - extract subpath
|
|
145
|
-
BASE_REPO="${BASH_REMATCH[1]}"
|
|
146
|
-
BRANCH="${BASH_REMATCH[2]}"
|
|
147
|
-
SUBPATH="${BASH_REMATCH[3]}"
|
|
148
|
-
CLEAN_URL="$BASE_REPO"
|
|
149
|
-
print_info "Detected subpath: $SUBPATH (branch: $BRANCH)"
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
# Extract owner/repo from URL for namespaced storage
|
|
153
|
-
# Supports: https://github.com/owner/repo or https://github.com/owner/repo.git
|
|
154
|
-
if [[ "$CLEAN_URL" =~ github\.com[/:]([^/]+)/([^/]+)(\.git)?$ ]]; then
|
|
155
|
-
REPO_OWNER="${BASH_REMATCH[1]}"
|
|
156
|
-
REPO_NAME="${BASH_REMATCH[2]%.git}"
|
|
157
|
-
else
|
|
158
|
-
# Fallback: just use basename
|
|
159
|
-
REPO_OWNER=""
|
|
160
|
-
REPO_NAME=$(basename "$CLEAN_URL" .git)
|
|
161
|
-
fi
|
|
162
|
-
|
|
163
|
-
if [ -n "$REPO_OWNER" ]; then
|
|
164
|
-
TARGET_CLONE_PATH="$DEFAULT_LIB_PATH/$REPO_OWNER/$REPO_NAME"
|
|
165
|
-
else
|
|
166
|
-
TARGET_CLONE_PATH="$DEFAULT_LIB_PATH/$REPO_NAME"
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
# Ensure library directory exists
|
|
170
|
-
mkdir -p "$DEFAULT_LIB_PATH"
|
|
171
|
-
|
|
172
|
-
if [ -d "$TARGET_CLONE_PATH" ]; then
|
|
173
|
-
print_warning "Directory already exists: $TARGET_CLONE_PATH"
|
|
174
|
-
read -p "Update with git pull? (y/N): " do_pull
|
|
175
|
-
if [[ "$do_pull" =~ ^[yY]$ ]]; then
|
|
176
|
-
print_info "Pulling latest changes..."
|
|
177
|
-
git -C "$TARGET_CLONE_PATH" pull
|
|
178
|
-
fi
|
|
179
|
-
else
|
|
180
|
-
print_info "Cloning $CLEAN_URL to $TARGET_CLONE_PATH..."
|
|
181
|
-
git clone "$CLEAN_URL" "$TARGET_CLONE_PATH"
|
|
182
|
-
if [ $? -ne 0 ]; then
|
|
183
|
-
print_error "Failed to clone repository"
|
|
184
|
-
exit 1
|
|
185
|
-
fi
|
|
186
|
-
print_success "Clone completed!"
|
|
187
|
-
fi
|
|
188
|
-
|
|
189
|
-
# If subpath was specified in URL, use it directly
|
|
190
|
-
if [ -n "$SUBPATH" ]; then
|
|
191
|
-
SKILL_PATH="$TARGET_CLONE_PATH/$SUBPATH"
|
|
192
|
-
if [ ! -d "$SKILL_PATH" ]; then
|
|
193
|
-
print_error "Subpath not found: $SKILL_PATH"
|
|
194
|
-
exit 1
|
|
195
|
-
fi
|
|
196
|
-
else
|
|
197
|
-
# Check if this is a multi-skill repo (has skills/ subdirectory)
|
|
198
|
-
SKILLS_DIR="$TARGET_CLONE_PATH/skills"
|
|
199
|
-
if [ -d "$SKILLS_DIR" ]; then
|
|
200
|
-
print_info "Detected multi-skill repository. Listing available skills..."
|
|
201
|
-
|
|
202
|
-
sub_skills=("$SKILLS_DIR"/*/)
|
|
203
|
-
if [ ${#sub_skills[@]} -eq 0 ]; then
|
|
204
|
-
print_warning "No skills found in $SKILLS_DIR, using repo root"
|
|
205
|
-
SKILL_PATH="$TARGET_CLONE_PATH"
|
|
206
|
-
else
|
|
207
|
-
# Extract skill names for display
|
|
208
|
-
sub_skill_names=()
|
|
209
|
-
for s in "${sub_skills[@]}"; do
|
|
210
|
-
sub_skill_names+=("$(basename "$s")")
|
|
211
|
-
done
|
|
212
|
-
|
|
213
|
-
echo ""
|
|
214
|
-
echo "Available Skills in this repo:"
|
|
215
|
-
select sub_skill_name in "${sub_skill_names[@]}" "Link entire repo"; do
|
|
216
|
-
if [ -n "$sub_skill_name" ]; then
|
|
217
|
-
if [ "$sub_skill_name" == "Link entire repo" ]; then
|
|
218
|
-
SKILL_PATH="$TARGET_CLONE_PATH"
|
|
219
|
-
else
|
|
220
|
-
SKILL_PATH="$SKILLS_DIR/$sub_skill_name"
|
|
221
|
-
fi
|
|
222
|
-
break
|
|
223
|
-
else
|
|
224
|
-
echo "Invalid selection. Please try again."
|
|
225
|
-
fi
|
|
226
|
-
done
|
|
227
|
-
fi
|
|
228
|
-
else
|
|
229
|
-
SKILL_PATH="$TARGET_CLONE_PATH"
|
|
230
|
-
fi
|
|
231
|
-
fi
|
|
232
|
-
fi
|
|
233
|
-
|
|
234
|
-
# 2. Determine Source Skill Path (if not already set by --from)
|
|
235
|
-
if [ -z "$SKILL_PATH" ]; then
|
|
236
|
-
# Check if default library exists
|
|
237
|
-
if [ -d "$DEFAULT_LIB_PATH" ]; then
|
|
238
|
-
print_info "No skill path provided. Checking default library: $DEFAULT_LIB_PATH"
|
|
239
|
-
skills=("$DEFAULT_LIB_PATH"/*/)
|
|
240
|
-
|
|
241
|
-
if [ ${#skills[@]} -eq 0 ]; then
|
|
242
|
-
print_error "No skills found in $DEFAULT_LIB_PATH"
|
|
243
|
-
print_info "Please provide a skill path: ./link-skill.sh <path_to_skill>"
|
|
244
|
-
exit 1
|
|
245
|
-
fi
|
|
246
|
-
|
|
247
|
-
# Extract skill names for display
|
|
248
|
-
skill_names=()
|
|
249
|
-
for s in "${skills[@]}"; do
|
|
250
|
-
skill_names+=("$(basename "$s")")
|
|
251
|
-
done
|
|
252
|
-
|
|
253
|
-
echo "Available Skills:"
|
|
254
|
-
select skill_name in "${skill_names[@]}"; do
|
|
255
|
-
if [ -n "$skill_name" ]; then
|
|
256
|
-
SKILL_PATH="${DEFAULT_LIB_PATH}/${skill_name}"
|
|
257
|
-
break
|
|
258
|
-
else
|
|
259
|
-
echo "Invalid selection. Please try again."
|
|
260
|
-
fi
|
|
261
|
-
done
|
|
262
|
-
else
|
|
263
|
-
# Fallback to current directory prompt
|
|
264
|
-
read -p "Enter path to skill directory (default: current dir): " input_path
|
|
265
|
-
input_path=${input_path:-.}
|
|
266
|
-
SKILL_PATH=$(realpath "$input_path")
|
|
267
|
-
fi
|
|
268
|
-
elif [ -n "$SKILL_PATH" ] && [ -z "$FROM_URL" ]; then
|
|
269
|
-
# Path provided directly as argument
|
|
270
|
-
SKILL_PATH=$(realpath "$SKILL_PATH")
|
|
271
|
-
fi
|
|
272
|
-
|
|
273
|
-
if [ ! -d "$SKILL_PATH" ]; then
|
|
274
|
-
print_error "Skill directory not found: $SKILL_PATH"
|
|
275
|
-
exit 1
|
|
276
|
-
fi
|
|
277
|
-
|
|
278
|
-
SKILL_NAME=$(basename "$SKILL_PATH")
|
|
279
|
-
print_info "Selected Skill: \033[1;36m$SKILL_NAME\033[0m ($SKILL_PATH)"
|
|
280
|
-
|
|
281
|
-
# 2. Define Supported Agents
|
|
282
|
-
# Format: "Name:ProjectDir:GlobalDir"
|
|
283
|
-
AGENTS=(
|
|
284
|
-
"Claude Code:.claude/skills:$HOME/.claude/skills"
|
|
285
|
-
"GitHub Copilot:.github/skills:$HOME/.copilot/skills"
|
|
286
|
-
"Google Antigravity:.agent/skills:$HOME/.gemini/antigravity/skills"
|
|
287
|
-
"Cursor:.cursor/skills:$HOME/.cursor/skills"
|
|
288
|
-
"OpenCode:.opencode/skill:$HOME/.config/opencode/skill"
|
|
289
|
-
"OpenAI Codex:.codex/skills:$HOME/.codex/skills"
|
|
290
|
-
"Gemini CLI:.gemini/skills:$HOME/.gemini/skills"
|
|
291
|
-
"Windsurf:.windsurf/skills:$HOME/.codeium/windsurf/skills"
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
# 3. Agent Selection
|
|
295
|
-
echo ""
|
|
296
|
-
echo "Select Agents to install to (Space to select, Enter to confirm):"
|
|
297
|
-
# Simple multi-select implementation using arrays
|
|
298
|
-
selected_indices=()
|
|
299
|
-
while true; do
|
|
300
|
-
for i in "${!AGENTS[@]}"; do
|
|
301
|
-
agent_info="${AGENTS[$i]}"
|
|
302
|
-
agent_name="${agent_info%%:*}"
|
|
303
|
-
|
|
304
|
-
# Check if selected
|
|
305
|
-
if [[ " ${selected_indices[*]} " =~ " $i " ]]; then
|
|
306
|
-
mark="[*]"
|
|
307
|
-
else
|
|
308
|
-
mark="[ ]"
|
|
309
|
-
fi
|
|
310
|
-
echo "$i) $mark $agent_name"
|
|
311
|
-
done
|
|
312
|
-
|
|
313
|
-
echo "a) Select All"
|
|
314
|
-
echo "d) Done"
|
|
315
|
-
read -p "Select option: " choice
|
|
316
|
-
|
|
317
|
-
if [[ "$choice" == "d" ]]; then
|
|
318
|
-
break
|
|
319
|
-
elif [[ "$choice" == "a" ]]; then
|
|
320
|
-
selected_indices=()
|
|
321
|
-
for i in "${!AGENTS[@]}"; do
|
|
322
|
-
selected_indices+=("$i")
|
|
323
|
-
done
|
|
324
|
-
elif [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 0 ] && [ "$choice" -lt "${#AGENTS[@]}" ]; then
|
|
325
|
-
if [[ " ${selected_indices[*]} " =~ " $choice " ]]; then
|
|
326
|
-
# Deselect
|
|
327
|
-
new_indices=()
|
|
328
|
-
for idx in "${selected_indices[@]}"; do
|
|
329
|
-
[[ "$idx" != "$choice" ]] && new_indices+=("$idx")
|
|
330
|
-
done
|
|
331
|
-
selected_indices=("${new_indices[@]}")
|
|
332
|
-
else
|
|
333
|
-
# Select
|
|
334
|
-
selected_indices+=("$choice")
|
|
335
|
-
fi
|
|
336
|
-
else
|
|
337
|
-
echo "Invalid choice."
|
|
338
|
-
fi
|
|
339
|
-
echo "------------------------"
|
|
340
|
-
done
|
|
341
|
-
|
|
342
|
-
if [ ${#selected_indices[@]} -eq 0 ]; then
|
|
343
|
-
print_warning "No agents selected. Exiting."
|
|
344
|
-
exit 0
|
|
345
|
-
fi
|
|
346
|
-
|
|
347
|
-
# 4. Process Each Selected Agent
|
|
348
|
-
for idx in "${selected_indices[@]}"; do
|
|
349
|
-
agent_raw="${AGENTS[$idx]}"
|
|
350
|
-
IFS=':' read -r agent_name project_dir global_dir <<< "$agent_raw"
|
|
351
|
-
|
|
352
|
-
echo ""
|
|
353
|
-
print_info "Configuring for \033[1;36m$agent_name\033[0m..."
|
|
354
|
-
|
|
355
|
-
# Scope Selection
|
|
356
|
-
echo "Select Scope:"
|
|
357
|
-
echo "1) Project ($project_dir)"
|
|
358
|
-
echo "2) Global ($global_dir)"
|
|
359
|
-
echo "3) Both"
|
|
360
|
-
echo "s) Skip"
|
|
361
|
-
read -p "Choice [1-3]: " scope_choice
|
|
362
|
-
|
|
363
|
-
targets=()
|
|
364
|
-
case $scope_choice in
|
|
365
|
-
1) targets+=("$project_dir") ;;
|
|
366
|
-
2) targets+=("$global_dir") ;;
|
|
367
|
-
3) targets+=("$project_dir" "$global_dir") ;;
|
|
368
|
-
s|S) continue ;;
|
|
369
|
-
*) print_warning "Invalid choice, skipping $agent_name"; continue ;;
|
|
370
|
-
esac
|
|
371
|
-
|
|
372
|
-
for target_base in "${targets[@]}"; do
|
|
373
|
-
# Resolve path expansion if needed (already expanded in definition for Global, Project is relative)
|
|
374
|
-
if [[ "$target_base" == /* ]]; then
|
|
375
|
-
# Absolute path (Global)
|
|
376
|
-
target_dir="$target_base"
|
|
377
|
-
else
|
|
378
|
-
# Relative path (Project) - assume current dir is project root
|
|
379
|
-
target_dir="$(pwd)/$target_base"
|
|
380
|
-
fi
|
|
381
|
-
|
|
382
|
-
# Ensure target parent directory exists
|
|
383
|
-
if [ ! -d "$target_dir" ]; then
|
|
384
|
-
print_info "Creating directory: $target_dir"
|
|
385
|
-
mkdir -p "$target_dir"
|
|
386
|
-
fi
|
|
387
|
-
|
|
388
|
-
target_link="$target_dir/$SKILL_NAME"
|
|
389
|
-
|
|
390
|
-
# Check for existing link or directory
|
|
391
|
-
if [ -e "$target_link" ] || [ -L "$target_link" ]; then
|
|
392
|
-
print_warning "$target_link already exists."
|
|
393
|
-
read -p "Overwrite? (y/N): " overwrite
|
|
394
|
-
if [[ "$overwrite" =~ ^[yY]$ ]]; then
|
|
395
|
-
rm -rf "$target_link"
|
|
396
|
-
else
|
|
397
|
-
print_info "Skipping..."
|
|
398
|
-
continue
|
|
399
|
-
fi
|
|
400
|
-
fi
|
|
401
|
-
|
|
402
|
-
# Create Symlink
|
|
403
|
-
ln -s "$SKILL_PATH" "$target_link"
|
|
404
|
-
if [ $? -eq 0 ]; then
|
|
405
|
-
print_success "Linked to $target_link"
|
|
406
|
-
else
|
|
407
|
-
print_error "Failed to link to $target_link"
|
|
408
|
-
fi
|
|
409
|
-
done
|
|
410
|
-
done
|
|
411
|
-
|
|
412
|
-
echo ""
|
|
413
|
-
print_success "All operations completed."
|