@vikasagarwal101/skill-sync 1.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.
@@ -0,0 +1,1250 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # skill-sync (bundled by scripts/bundle.sh)
5
+ # Source: https://github.com/vikasagarwal101/skill-sync
6
+
7
+ # -- Configuration --
8
+
9
+ CONFIG_DIR="${SKILLSYNC_CONFIG_DIR:-$HOME/.config/skill-sync}"
10
+ MANIFEST="$CONFIG_DIR/manifest"
11
+ CONFIG_FILE="$CONFIG_DIR/config"
12
+
13
+ # Defaults (overridden by config file if present)
14
+ declare -A SOURCES=(
15
+ [agents]="${SKILLSYNC_AGENTS_DIR:-$HOME/.agents/skills}"
16
+ [opencode]="${SKILLSYNC_OPENCODE_DIR:-$HOME/.config/opencode/skills}"
17
+ [codex]="${SKILLSYNC_CODEX_DIR:-$HOME/.codex/skills}"
18
+ )
19
+
20
+ declare -A TARGETS=(
21
+ [claude]="${SKILLSYNC_CLAUDE_DIR:-$HOME/.claude/skills}"
22
+ )
23
+
24
+ SOURCE_PRIORITY=("agents" "opencode" "codex")
25
+
26
+ # Load config file if it exists (overrides defaults)
27
+ [ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE"
28
+
29
+ # Global flags
30
+ DRY_RUN=0
31
+
32
+ # -- Helpers --
33
+
34
+ # Safe operations that respect --dry-run
35
+ _safe_cp() { [ $DRY_RUN -eq 1 ] && echo " [dry-run] cp -r $1 $2" || cp -r "$1" "$2"; }
36
+ _safe_rm() { [ $DRY_RUN -eq 1 ] && echo " [dry-run] rm -rf $1" || rm -rf "$1"; }
37
+ _safe_mv() { [ $DRY_RUN -eq 1 ] && echo " [dry-run] mv $1 $2" || mv "$1" "$2"; }
38
+
39
+ md5_content() {
40
+ find "$1" -type f -exec md5sum {} \; 2>/dev/null | awk '{print $1}' | sort | md5sum | cut -d' ' -f1
41
+ }
42
+
43
+ dir_exists() { [ -d "$1" ]; }
44
+
45
+ source_for_skill() {
46
+ local name="$1"
47
+ for label in "${SOURCE_PRIORITY[@]}"; do
48
+ local dir="${SOURCES[$label]}"
49
+ if [ -f "$dir/$name/SKILL.md" ] || [ -d "$dir/$name" ]; then
50
+ echo "$label"
51
+ return 0
52
+ fi
53
+ done
54
+ return 1
55
+ }
56
+
57
+ source_path_for_skill() {
58
+ local name="$1"
59
+ for label in "${SOURCE_PRIORITY[@]}"; do
60
+ local dir="${SOURCES[$label]}"
61
+ if [ -f "$dir/$name/SKILL.md" ] || [ -d "$dir/$name" ]; then
62
+ echo "$dir/$name"
63
+ return 0
64
+ fi
65
+ done
66
+ return 1
67
+ }
68
+
69
+ all_skill_names() {
70
+ declare -A seen
71
+ for label in "${SOURCE_PRIORITY[@]}"; do
72
+ local dir="${SOURCES[$label]}"
73
+ dir_exists "$dir" || continue
74
+ for d in "$dir"/*/; do
75
+ [ -d "$d" ] || continue
76
+ local n
77
+ n=$(basename "$d")
78
+ [ "$n" = ".system" ] && continue
79
+ if [ -f "$d/SKILL.md" ]; then
80
+ [ -n "${seen[$n]:-}" ] && continue
81
+ seen[$n]=1
82
+ echo "$n"
83
+ else
84
+ for sub in "$d"*/; do
85
+ [ -d "$sub" ] || continue
86
+ [ -f "$sub/SKILL.md" ] || continue
87
+ local sn nested
88
+ sn=$(basename "$sub")
89
+ nested="$n/$sn"
90
+ [ -n "${seen[$nested]:-}" ] && continue
91
+ seen[$nested]=1
92
+ echo "$nested"
93
+ done
94
+ fi
95
+ done
96
+ done
97
+ for label in "${!TARGETS[@]}"; do
98
+ local dir="${TARGETS[$label]}"
99
+ dir_exists "$dir" || continue
100
+ for d in "$dir"/*/; do
101
+ [ -d "$d" ] || continue
102
+ local n
103
+ n=$(basename "$d")
104
+ if [ -f "$d/SKILL.md" ]; then
105
+ [ -n "${seen[$n]:-}" ] && continue
106
+ seen[$n]=1
107
+ echo "$n"
108
+ else
109
+ for sub in "$d"*/; do
110
+ [ -d "$sub" ] || continue
111
+ [ -f "$sub/SKILL.md" ] || continue
112
+ local sn nested
113
+ sn=$(basename "$sub")
114
+ nested="$n/$sn"
115
+ [ -n "${seen[$nested]:-}" ] && continue
116
+ seen[$nested]=1
117
+ echo "$nested"
118
+ done
119
+ fi
120
+ done
121
+ done
122
+ }
123
+
124
+ extract_frontmatter_field() {
125
+ local file="$1"
126
+ local field="$2"
127
+ [ -f "$file" ] || return 1
128
+ awk -v field="$field" '
129
+ BEGIN { found=0; block=""; is_block=0 }
130
+ /^---$/ {
131
+ if (found && is_block) { print block; exit }
132
+ in_fm = !in_fm; next
133
+ }
134
+ in_fm && !found && $0 ~ "^"field":" {
135
+ val = $0
136
+ sub("^"field":\\s*", "", val)
137
+ if (val == ">" || val == ">-" || val == "|" || val == "|-") {
138
+ is_block = 1; found = 1; next
139
+ }
140
+ gsub(/^["'\'']|["'\'']$/, "", val)
141
+ if (val == ">") val = ""
142
+ print val
143
+ exit
144
+ }
145
+ found && is_block && /^[ \t]/ {
146
+ line = $0; sub(/^[ \t]+/, "", line)
147
+ if (block == "") block = line
148
+ else block = block " " line
149
+ next
150
+ }
151
+ found && is_block && /^[^ \t]/ { print block; exit }
152
+ END { if (found && is_block) print block }
153
+ ' "$file"
154
+ }
155
+
156
+ has_frontmatter() {
157
+ local file="$1"
158
+ [ -f "$file" ] || return 1
159
+ head -1 "$file" 2>/dev/null | grep -q '^---$'
160
+ }
161
+
162
+ # -- Manifest --
163
+
164
+ ensure_manifest() {
165
+ mkdir -p "$CONFIG_DIR"
166
+ if [ ! -f "$MANIFEST" ]; then
167
+ cmd_init
168
+ fi
169
+ }
170
+
171
+ manifest_get_field() {
172
+ local name="$1" field="$2"
173
+ [ -f "$MANIFEST" ] || return 1
174
+ awk -F'|' -v name="$name" -v field="$field" '
175
+ NR <= 4 { next }
176
+ { gsub(/[ \t]+/, "", $1); gsub(/[ \t]+/, "", $2); gsub(/[ \t]+/, "", $3) }
177
+ $1 == name {
178
+ if (field == "source") print $2
179
+ else if (field == "targets") print $3
180
+ exit
181
+ }
182
+ ' "$MANIFEST"
183
+ }
184
+
185
+ manifest_has() {
186
+ local name="$1"
187
+ [ -f "$MANIFEST" ] || return 1
188
+ awk -F'|' -v name="$name" '
189
+ NR <= 4 { next }
190
+ { gsub(/[ \t]+/, "", $1) }
191
+ $1 == name { found=1; exit }
192
+ END { exit !found }
193
+ ' "$MANIFEST"
194
+ }
195
+
196
+ manifest_add() {
197
+ local name="$1" source="$2" targets="$3"
198
+ ensure_manifest
199
+ if manifest_has "$name"; then
200
+ local tmp
201
+ tmp=$(mktemp)
202
+ awk -F'|' -v name="$name" -v src="$source" -v tgt="$targets" '
203
+ NR <= 4 { print; next }
204
+ { gsub(/[ \t]+/, "", $1) }
205
+ $1 == name { printf "%-30s | %-10s | %s\n", name, src, tgt; next }
206
+ { print }
207
+ ' "$MANIFEST" > "$tmp"
208
+ mv "$tmp" "$MANIFEST"
209
+ else
210
+ printf "%-30s | %-10s | %s\n" "$name" "$source" "$targets" >> "$MANIFEST"
211
+ fi
212
+ }
213
+
214
+ manifest_remove() {
215
+ local name="$1"
216
+ [ -f "$MANIFEST" ] || return 0
217
+ local tmp
218
+ tmp=$(mktemp)
219
+ awk -F'|' -v name="$name" '
220
+ NR <= 4 { print; next }
221
+ { gsub(/[ \t]+/, "", $1) }
222
+ $1 != name { print }
223
+ ' "$MANIFEST" > "$tmp"
224
+ mv "$tmp" "$MANIFEST"
225
+ }
226
+
227
+
228
+
229
+ cmd_init() {
230
+ mkdir -p "$CONFIG_DIR"
231
+ echo "# skill-sync manifest" > "$MANIFEST"
232
+ echo "# Format: skill_name | source | targets" >> "$MANIFEST"
233
+ echo "# Sources: agents, opencode, codex | Targets: claude (or - for canonical-only)" >> "$MANIFEST"
234
+ echo "#" >> "$MANIFEST"
235
+
236
+ # Build from current state
237
+ local added=0
238
+ for name in $(all_skill_names | sort); do
239
+ local src
240
+ src=$(source_for_skill "$name" 2>/dev/null || echo "orphan")
241
+
242
+ # Determine targets: which target dirs contain this skill
243
+ local tgt_list="-"
244
+ for tlabel in "${!TARGETS[@]}"; do
245
+ local tdir="${TARGETS[$tlabel]}"
246
+ if dir_exists "$tdir/$name"; then
247
+ if [ "$tgt_list" = "-" ]; then
248
+ tgt_list="$tlabel"
249
+ else
250
+ tgt_list="$tgt_list,$tlabel"
251
+ fi
252
+ fi
253
+ done
254
+
255
+ # Skip codex system skills
256
+ [ "$src" = "codex" ] && {
257
+ local cdir="${SOURCES[codex]}"
258
+ [ -d "$cdir/.system/$name" ] && continue
259
+ }
260
+
261
+ printf "%-30s | %-10s | %s\n" "$name" "$src" "$tgt_list" >> "$MANIFEST"
262
+ added=$((added + 1))
263
+ done
264
+
265
+ echo "Manifest generated: $MANIFEST"
266
+ echo " $added skills indexed"
267
+ }
268
+
269
+ cmd_sync_all() {
270
+ ensure_manifest
271
+ local total=0 synced=0 updated=0 failed=0
272
+
273
+ echo "Syncing all manifest skills..."
274
+ echo ""
275
+
276
+ while IFS='|' read -r name source targets; do
277
+ # Skip comments/headers
278
+ name=$(echo "$name" | xargs)
279
+ [[ "$name" =~ ^# ]] && continue
280
+ [ -z "$name" ] && continue
281
+ source=$(echo "$source" | xargs)
282
+ targets=$(echo "$targets" | xargs)
283
+ [ "$targets" = "-" ] && continue
284
+
285
+ total=$((total + 1))
286
+
287
+ local src_path
288
+ src_path=$(source_path_for_skill "$name") || {
289
+ echo " ⚠️ $name — source not found"
290
+ failed=$((failed + 1))
291
+ continue
292
+ }
293
+
294
+ # Sync to each target
295
+ IFS=',' read -ra tgt_arr <<< "$targets"
296
+ for tlabel in "${tgt_arr[@]}"; do
297
+ tlabel=$(echo "$tlabel" | xargs)
298
+ local tdir="${TARGETS[$tlabel]}"
299
+ [ -d "$tdir" ] || mkdir -p "$tdir"
300
+
301
+ local src_hash tgt_hash
302
+ src_hash=$(md5_content "$src_path")
303
+ if [ -d "$tdir/$name" ]; then
304
+ tgt_hash=$(md5_content "$tdir/$name")
305
+ else
306
+ tgt_hash="none"
307
+ fi
308
+
309
+ if [ "$src_hash" = "$tgt_hash" ]; then
310
+ echo " ✅ $name → $tlabel (in sync)"
311
+ synced=$((synced + 1))
312
+ else
313
+ _safe_rm "$tdir/$name"
314
+ _safe_cp "$src_path" "$tdir/$name"
315
+ echo " 🔄 $name → $tlabel (updated)"
316
+ updated=$((updated + 1))
317
+ fi
318
+ done
319
+ done < "$MANIFEST"
320
+
321
+ echo ""
322
+ echo "Done: $total skills, $updated updated, $synced already in sync, $failed failed"
323
+ }
324
+
325
+ cmd_sync_one() {
326
+ local name="$1"
327
+ ensure_manifest
328
+
329
+ local targets
330
+ targets=$(manifest_get_field "$name" "targets")
331
+ [ -z "$targets" ] && {
332
+ echo "'$name' not in manifest. Use: skill-sync add $name"
333
+ return 1
334
+ }
335
+ [ "$targets" = "-" ] && {
336
+ echo "'$name' has no sync targets (canonical-only)."
337
+ return 0
338
+ }
339
+
340
+ local src_path
341
+ src_path=$(source_path_for_skill "$name") || {
342
+ echo "Source not found for '$name'"
343
+ return 1
344
+ }
345
+
346
+ IFS=',' read -ra tgt_arr <<< "$targets"
347
+ for tlabel in "${tgt_arr[@]}"; do
348
+ tlabel=$(echo "$tlabel" | xargs)
349
+ local tdir="${TARGETS[$tlabel]}"
350
+ [ -d "$tdir" ] || mkdir -p "$tdir"
351
+
352
+ if [ -d "$tdir/$name" ] && [ "$(md5_content "$src_path")" = "$(md5_content "$tdir/$name")" ]; then
353
+ echo "✅ $name → $tlabel (already in sync)"
354
+ else
355
+ _safe_rm "$tdir/$name"
356
+ _safe_cp "$src_path" "$tdir/$name"
357
+ echo "🔄 $name → $tlabel (updated)"
358
+ fi
359
+ done
360
+ }
361
+
362
+ cmd_add() {
363
+ local name="$1"
364
+ shift || true
365
+ local to_target="claude"
366
+
367
+ while [[ $# -gt 0 ]]; do
368
+ case "$1" in
369
+ --to) to_target="$2"; shift 2 ;;
370
+ *) shift ;;
371
+ esac
372
+ done
373
+
374
+ local src_label
375
+ src_label=$(source_for_skill "$name" 2>/dev/null) || {
376
+ echo "Skill '$name' not found in any source:"
377
+ for l in "${SOURCE_PRIORITY[@]}"; do echo " ${SOURCES[$l]}/$name/"; done
378
+ return 1
379
+ }
380
+
381
+ local src_path
382
+ src_path=$(source_path_for_skill "$name")
383
+
384
+ local tdir="${TARGETS[$to_target]}"
385
+ [ -d "$tdir" ] || mkdir -p "$tdir"
386
+
387
+ _safe_rm "$tdir/$name"
388
+ _safe_cp "$src_path" "$tdir/$name"
389
+
390
+ manifest_add "$name" "$src_label" "$to_target"
391
+ echo "✅ '$name' added → $to_target (from $src_label)"
392
+ }
393
+
394
+ cmd_remove() {
395
+ local name="$1"
396
+ shift || true
397
+ local from_target="" purge=0
398
+
399
+ while [[ $# -gt 0 ]]; do
400
+ case "$1" in
401
+ --from) from_target="$2"; shift 2 ;;
402
+ --purge) purge=1; shift ;;
403
+ *) shift ;;
404
+ esac
405
+ done
406
+
407
+ # Safety: resolve the skill and show where it currently exists
408
+ local src_label="" src_path=""
409
+ src_label=$(source_for_skill "$name" 2>/dev/null) && src_path=$(source_path_for_skill "$name")
410
+
411
+ echo "Removing: $name"
412
+ echo ""
413
+
414
+ # Show current state
415
+ if [ -n "$src_path" ]; then
416
+ echo " Source: $src_label ($src_path)"
417
+ else
418
+ echo " Source: — (no canonical source)"
419
+ fi
420
+
421
+ local target_hits=""
422
+ for tlabel in "${!TARGETS[@]}"; do
423
+ if [ -d "${TARGETS[$tlabel]}/$name" ]; then
424
+ target_hits="$target_hits $tlabel"
425
+ fi
426
+ done
427
+ if [ -n "$target_hits" ]; then
428
+ echo " Targets:$target_hits"
429
+ else
430
+ echo " Targets: — (not deployed)"
431
+ fi
432
+ echo ""
433
+
434
+ # If skill doesn't exist anywhere
435
+ if [ -z "$src_path" ] && [ -z "$target_hits" ]; then
436
+ echo "Skill '$name' not found anywhere."
437
+ manifest_remove "$name"
438
+ return 0
439
+ fi
440
+
441
+ # Phase 1: Remove from targets
442
+ local removed_any=0
443
+
444
+ if [ -n "$from_target" ]; then
445
+ # Remove from specific target only
446
+ local tdir="${TARGETS[$from_target]:-}"
447
+ if [ -z "$tdir" ]; then
448
+ echo "Unknown target '$from_target'"
449
+ return 1
450
+ fi
451
+ if [ -d "$tdir/$name" ]; then
452
+ _safe_rm "$tdir/$name"
453
+ echo " 🗑️ Removed from $from_target"
454
+ removed_any=1
455
+ else
456
+ echo " '$name' was not in $from_target"
457
+ fi
458
+
459
+ # Update manifest: remove this target from the list
460
+ local current_targets
461
+ current_targets=$(manifest_get_field "$name" "targets")
462
+ local new_targets=""
463
+ IFS=',' read -ra remain <<< "$current_targets"
464
+ for t in "${remain[@]}"; do
465
+ t=$(echo "$t" | xargs)
466
+ [ "$t" = "$from_target" ] && continue
467
+ if [ -z "$new_targets" ]; then new_targets="$t"; else new_targets="$new_targets,$t"; fi
468
+ done
469
+ [ -z "$new_targets" ] && new_targets="-"
470
+ [ -n "$src_label" ] && manifest_add "$name" "$src_label" "$new_targets"
471
+ else
472
+ # Remove from ALL targets
473
+ for tlabel in "${!TARGETS[@]}"; do
474
+ local tdir="${TARGETS[$tlabel]}"
475
+ if [ -d "$tdir/$name" ]; then
476
+ _safe_rm "$tdir/$name"
477
+ echo " 🗑️ Removed from $tlabel"
478
+ removed_any=1
479
+ fi
480
+ done
481
+ [ $removed_any -eq 0 ] && echo " (was not deployed to any target)"
482
+
483
+ # Update manifest: set targets to -
484
+ [ -n "$src_label" ] && manifest_add "$name" "$src_label" "-"
485
+ fi
486
+
487
+ # Phase 2: Purge from canonical source
488
+ if [ $purge -eq 1 ]; then
489
+ if [ -n "$src_path" ] && [ -d "$src_path" ]; then
490
+ _safe_rm "$src_path"
491
+ echo " 💀 Purged from $src_label source"
492
+ manifest_remove "$name"
493
+ elif [ -z "$src_path" ]; then
494
+ echo " (no canonical source to purge)"
495
+ fi
496
+ fi
497
+
498
+ echo ""
499
+ echo "Done."
500
+
501
+ # Show what remains
502
+ if [ $purge -eq 0 ] && [ -n "$src_label" ]; then
503
+ echo " Skill still exists in source: $src_label"
504
+ echo " Re-deploy with: skill-sync add $name"
505
+ elif [ $purge -eq 1 ]; then
506
+ echo " Skill fully removed from all locations."
507
+ fi
508
+ }
509
+
510
+
511
+ cmd_create() {
512
+ local name="$1"
513
+ shift || true
514
+ local scope="agents"
515
+
516
+ while [[ $# -gt 0 ]]; do
517
+ case "$1" in
518
+ --scope) scope="$2"; shift 2 ;;
519
+ --to)
520
+ local to_target="$2"; shift 2
521
+ ;;
522
+ *) shift ;;
523
+ esac
524
+ done
525
+
526
+ # Validate scope
527
+ [ -z "${SOURCES[$scope]:-}" ] && {
528
+ echo "Unknown scope '$scope'. Use: agents, opencode, codex"
529
+ return 1
530
+ }
531
+
532
+ local dir="${SOURCES[$scope]}/$name"
533
+ [ -d "$dir" ] && {
534
+ echo "'$name' already exists at $dir"
535
+ return 1
536
+ }
537
+
538
+ mkdir -p "$dir"
539
+ cat > "$dir/SKILL.md" << EOF
540
+ ---
541
+ name: $name
542
+ description: TODO — describe what this skill does and when to trigger it. Include specific use cases and example phrases the user might say.
543
+ ---
544
+
545
+ # $(echo "$name" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)$i=toupper(substr($i,1,1))substr($i,2)}1')
546
+
547
+ ## When to Use
548
+
549
+ TODO — Describe the specific scenarios where this skill should be triggered.
550
+
551
+ ## Workflow
552
+
553
+ TODO — Describe the step-by-step process.
554
+
555
+ ## Key Rules
556
+
557
+ TODO — List any non-negotiable rules or constraints.
558
+ EOF
559
+
560
+ manifest_add "$name" "$scope" "-"
561
+ echo "✅ Created '$name' in $scope"
562
+ echo " $dir/SKILL.md"
563
+ echo ""
564
+ echo " Edit the SKILL.md, then deploy with:"
565
+ echo " skill-sync add $name --to claude"
566
+ }
567
+
568
+ cmd_edit() {
569
+ local name="$1"
570
+ local src_path
571
+ src_path=$(source_path_for_skill "$name") || {
572
+ echo "'$name' not found in any canonical source"
573
+ return 1
574
+ }
575
+
576
+ local skill_md="$src_path/SKILL.md"
577
+ [ -f "$skill_md" ] || {
578
+ echo "No SKILL.md at $src_path"
579
+ return 1
580
+ }
581
+
582
+ local editor="${EDITOR:-vi}"
583
+ echo "Opening $skill_md with $editor..."
584
+ "$editor" "$skill_md"
585
+
586
+ # After editor exits, offer to sync if skill has targets
587
+ local targets
588
+ targets=$(manifest_get_field "$name" "targets" 2>/dev/null)
589
+ if [ -n "$targets" ] && [ "$targets" != "-" ]; then
590
+ echo ""
591
+ echo "Skill '$name' has targets: $targets"
592
+ echo "Run 'skill-sync sync $name' to push changes."
593
+ fi
594
+ }
595
+
596
+ cmd_pull() {
597
+ local name="$1"
598
+ shift || true
599
+ local from_target=""
600
+
601
+ while [[ $# -gt 0 ]]; do
602
+ case "$1" in
603
+ --from) from_target="$2"; shift 2 ;;
604
+ *) shift ;;
605
+ esac
606
+ done
607
+
608
+ local src_path
609
+ src_path=$(source_path_for_skill "$name") || {
610
+ echo "Canonical source for '$name' not found"
611
+ return 1
612
+ }
613
+
614
+ # Find which target to pull from
615
+ local tlabel tdir
616
+ if [ -n "$from_target" ]; then
617
+ tlabel="$from_target"
618
+ tdir="${TARGETS[$tlabel]:-}"
619
+ [ -z "$tdir" ] && { echo "Unknown target '$tlabel'"; return 1; }
620
+ else
621
+ # Auto-detect: find first target that has this skill
622
+ for tl in "${!TARGETS[@]}"; do
623
+ if [ -d "${TARGETS[$tl]}/$name" ]; then
624
+ tlabel="$tl"
625
+ tdir="${TARGETS[$tl]}"
626
+ break
627
+ fi
628
+ done
629
+ fi
630
+
631
+ [ -z "${tlabel:-}" ] && { echo "'$name' not found in any target"; return 1; }
632
+
633
+ if [ ! -d "$tdir/$name" ]; then
634
+ echo "'$name' not in $tlabel"
635
+ return 1
636
+ fi
637
+
638
+ local src_hash tgt_hash
639
+ src_hash=$(md5_content "$src_path")
640
+ tgt_hash=$(md5_content "$tdir/$name")
641
+
642
+ if [ "$src_hash" = "$tgt_hash" ]; then
643
+ echo "✅ Already in sync — no changes to pull"
644
+ return 0
645
+ fi
646
+
647
+ echo "Pulling '$name' from $tlabel → canonical source..."
648
+ _safe_rm "$src_path"
649
+ _safe_cp "$tdir/$name" "$src_path"
650
+ echo "✅ Pulled. Canonical source updated from $tlabel copy."
651
+ echo " Run 'skill-sync sync' to push to other targets."
652
+ }
653
+
654
+ cmd_rename() {
655
+ local old_name="$1"
656
+ local new_name="$2"
657
+
658
+ [ -z "$new_name" ] && { echo "Usage: skill-sync rename <old> <new>"; return 1; }
659
+
660
+ # Validate new name
661
+ [[ "$new_name" =~ ^[a-z0-9][-a-z0-9/]*$ ]] || {
662
+ echo "Invalid name '$new_name' — use lowercase, hyphens, digits only"
663
+ return 1
664
+ }
665
+
666
+ local src_label src_path
667
+ src_label=$(source_for_skill "$old_name" 2>/dev/null) || {
668
+ echo "'$old_name' not found in any source"
669
+ return 1
670
+ }
671
+ src_path=$(source_path_for_skill "$old_name")
672
+
673
+ # Check new name doesn't already exist
674
+ if source_for_skill "$new_name" >/dev/null 2>&1; then
675
+ echo "'$new_name' already exists"
676
+ return 1
677
+ fi
678
+
679
+ echo "Renaming: $old_name → $new_name"
680
+ echo ""
681
+
682
+ # 1. Rename canonical source
683
+ local src_parent
684
+ src_parent=$(dirname "$src_path")
685
+ _safe_mv "$src_path" "$src_parent/$new_name"
686
+ echo " 📝 Renamed in $src_label source"
687
+
688
+ # 2. Rename in all targets
689
+ for tlabel in "${!TARGETS[@]}"; do
690
+ local tdir="${TARGETS[$tlabel]}"
691
+ if [ -d "$tdir/$old_name" ]; then
692
+ _safe_rm "$tdir/$old_name"
693
+ _safe_cp "$src_parent/$new_name" "$tdir/$new_name"
694
+ echo " 🔄 Updated in $tlabel"
695
+ fi
696
+ done
697
+
698
+ # 3. Update manifest
699
+ manifest_remove "$old_name"
700
+ local targets="-"
701
+ for tlabel in "${!TARGETS[@]}"; do
702
+ if [ -d "${TARGETS[$tlabel]}/$new_name" ]; then
703
+ [ "$targets" = "-" ] && targets="$tlabel" || targets="$targets,$tlabel"
704
+ fi
705
+ done
706
+ manifest_add "$new_name" "$src_label" "$targets"
707
+
708
+ echo ""
709
+ echo "✅ Renamed $old_name → $new_name across all locations"
710
+ }
711
+
712
+ cmd_info() {
713
+ local name="$1"
714
+
715
+ local src_label
716
+ src_label=$(source_for_skill "$name" 2>/dev/null) || {
717
+ echo "'$name' not found in any canonical source"
718
+ # Check if it's orphaned in a target
719
+ for tlabel in "${!TARGETS[@]}"; do
720
+ if [ -d "${TARGETS[$tlabel]}/$name" ]; then
721
+ echo " ⚠️ Found orphaned copy in $tlabel (no canonical source)"
722
+ fi
723
+ done
724
+ return 1
725
+ }
726
+
727
+ local src_path
728
+ src_path=$(source_path_for_skill "$name")
729
+ local desc
730
+ desc=$(extract_frontmatter_field "$src_path/SKILL.md" "description")
731
+ [ -z "$desc" ] && desc="(no description)"
732
+
733
+ echo "$name"
734
+ echo " Source: $src_label ($src_path)"
735
+ echo " Description: $desc"
736
+
737
+ # Size and files
738
+ local size files
739
+ size=$(du -sh "$src_path" 2>/dev/null | cut -f1)
740
+ files=$(find "$src_path" -type f | wc -l)
741
+ echo " Files: $files ($size)"
742
+
743
+ # Frontmatter health
744
+ if has_frontmatter "$src_path/SKILL.md"; then
745
+ echo " Frontmatter: ✅ present"
746
+ else
747
+ echo " Frontmatter: ❌ MISSING"
748
+ fi
749
+
750
+ # Targets / sync state
751
+ local manifest_targets="-"
752
+ [ -f "$MANIFEST" ] && manifest_targets=$(manifest_get_field "$name" "targets")
753
+ [ -z "$manifest_targets" ] && manifest_targets="(not in manifest)"
754
+ echo " Manifest: $manifest_targets"
755
+
756
+ echo " Copies:"
757
+ for tlabel in "${!TARGETS[@]}"; do
758
+ local tdir="${TARGETS[$tlabel]}"
759
+ if [ -d "$tdir/$name" ]; then
760
+ local sh th
761
+ sh=$(md5_content "$src_path")
762
+ th=$(md5_content "$tdir/$name")
763
+ if [ "$sh" = "$th" ]; then
764
+ echo " $tlabel: ✅ in sync"
765
+ else
766
+ echo " $tlabel: 🔄 DRIFTED"
767
+ fi
768
+ else
769
+ echo " $tlabel: — (not deployed)"
770
+ fi
771
+ done
772
+ }
773
+
774
+ cmd_status() {
775
+ ensure_manifest
776
+ printf "%-30s %-10s %-15s %s\n" "SKILL" "SOURCE" "TARGETS" "STATE"
777
+ printf "%-30s %-10s %-15s %s\n" "$(printf '%.0s-' {1..30})" "$(printf '%.0s-' {1..10})" "$(printf '%.0s-' {1..15})" "$(printf '%.0s-' {1..5})"
778
+
779
+ local all_ok=1
780
+
781
+ while IFS='|' read -r name source targets; do
782
+ name=$(echo "$name" | xargs)
783
+ [[ "$name" =~ ^# ]] && continue
784
+ [ -z "$name" ] && continue
785
+ source=$(echo "$source" | xargs)
786
+ targets=$(echo "$targets" | xargs)
787
+
788
+ local state=""
789
+ if [ "$targets" = "-" ]; then
790
+ state="canonical"
791
+ else
792
+ IFS=',' read -ra tgt_arr <<< "$targets"
793
+ for tlabel in "${tgt_arr[@]}"; do
794
+ tlabel=$(echo "$tlabel" | xargs)
795
+ local tdir="${TARGETS[$tlabel]}"
796
+ local src_path
797
+ src_path=$(source_path_for_skill "$name" 2>/dev/null)
798
+ if [ -z "$src_path" ]; then
799
+ state="$state ⚠️"
800
+ all_ok=0
801
+ elif [ -d "$tdir/$name" ]; then
802
+ if [ "$(md5_content "$src_path")" = "$(md5_content "$tdir/$name")" ]; then
803
+ state="$state ✅"
804
+ else
805
+ state="$state 🔄"
806
+ all_ok=0
807
+ fi
808
+ else
809
+ state="$state ❌"
810
+ all_ok=0
811
+ fi
812
+ done
813
+ fi
814
+
815
+ printf "%-30s %-10s %-15s %s\n" "$name" "$source" "$targets" "$state"
816
+ done < "$MANIFEST"
817
+
818
+ echo ""
819
+ [ $all_ok -eq 1 ] && echo "All synced ✅" || echo "Issues found — run 'skill-sync sync' to fix"
820
+ }
821
+
822
+ cmd_list() {
823
+ local show_all="${1:-}"
824
+
825
+ if [ "$show_all" != "--all" ]; then
826
+ # List manifest skills only
827
+ ensure_manifest
828
+ printf "%-30s %-10s %s\n" "SKILL" "SOURCE" "TARGETS"
829
+ printf "%-30s %-10s %s\n" "-----" "------" "-------"
830
+ while IFS='|' read -r name source targets; do
831
+ name=$(echo "$name" | xargs)
832
+ [[ "$name" =~ ^# ]] && continue
833
+ [ -z "$name" ] && continue
834
+ source=$(echo "$source" | xargs)
835
+ targets=$(echo "$targets" | xargs)
836
+ [ "$targets" != "-" ] && printf "%-30s %-10s %s\n" "$name" "$source" "$targets"
837
+ done < "$MANIFEST"
838
+ return
839
+ fi
840
+
841
+ # List ALL skills from all locations
842
+ printf "%-30s %-10s %-10s %s\n" "SKILL" "SOURCE" "COPIES" "DESCRIPTION"
843
+ printf "%-30s %-10s %-10s %s\n" "-----" "------" "------" "-----------"
844
+
845
+ for name in $(all_skill_names | sort); do
846
+ local src_label src_path copies=""
847
+ src_label=$(source_for_skill "$name" 2>/dev/null) || src_label="orphan"
848
+ src_path=$(source_path_for_skill "$name" 2>/dev/null) || src_path=""
849
+
850
+ for tlabel in "${!TARGETS[@]}"; do
851
+ [ -d "${TARGETS[$tlabel]}/$name" ] && copies="$copies $tlabel"
852
+ done
853
+ [ -z "$copies" ] && copies="-"
854
+
855
+ local desc=""
856
+ if [ -n "$src_path" ] && [ -f "$src_path/SKILL.md" ]; then
857
+ desc=$(extract_frontmatter_field "$src_path/SKILL.md" "description" 2>/dev/null)
858
+ fi
859
+ [ -z "$desc" ] && desc="—"
860
+
861
+ printf "%-30s %-10s %-10s %.60s\n" "$name" "$src_label" "$copies" "$desc"
862
+ done
863
+ }
864
+
865
+ cmd_search() {
866
+ local query="$*"
867
+ [ -z "$query" ] && { echo "Usage: skill-sync search <query>"; return 1; }
868
+ query=$(echo "$query" | tr '[:upper:]' '[:lower:]')
869
+
870
+ printf "%-30s %-10s %.70s\n" "SKILL" "SOURCE" "DESCRIPTION"
871
+ printf "%-30s %-10s %.70s\n" "-----" "------" "-----------"
872
+
873
+ local found=0
874
+ for name in $(all_skill_names | sort); do
875
+ local src_path desc lname
876
+ src_path=$(source_path_for_skill "$name" 2>/dev/null) || continue
877
+ desc=$(extract_frontmatter_field "$src_path/SKILL.md" "description" 2>/dev/null)
878
+ [ -z "$desc" ] && desc="—"
879
+ lname=$(echo "$name $desc" | tr '[:upper:]' '[:lower:]')
880
+
881
+ if [[ "$lname" == *"$query"* ]]; then
882
+ local src_label
883
+ src_label=$(source_for_skill "$name")
884
+ printf "%-30s %-10s %.70s\n" "$name" "$src_label" "$desc"
885
+ found=$((found + 1))
886
+ fi
887
+ done
888
+
889
+ echo ""
890
+ [ $found -eq 0 ] && echo "No matches for '$query'" || echo "$found skill(s) found"
891
+ }
892
+
893
+ cmd_diff() {
894
+ local name="$1"
895
+
896
+ local src_path
897
+ src_path=$(source_path_for_skill "$name") || {
898
+ echo "'$name' not found in any source"
899
+ return 1
900
+ }
901
+
902
+ local has_targets=0
903
+ for tlabel in "${!TARGETS[@]}"; do
904
+ if [ -d "${TARGETS[$tlabel]}/$name" ]; then
905
+ has_targets=1
906
+ echo "── diff: $name (source → $tlabel) ──"
907
+ diff -rq "$src_path" "${TARGETS[$tlabel]}/$name" 2>/dev/null || true
908
+ echo ""
909
+
910
+ # Show SKILL.md content diff if different
911
+ if ! diff -q "$src_path/SKILL.md" "${TARGETS[$tlabel]}/$name/SKILL.md" >/dev/null 2>&1; then
912
+ echo "── SKILL.md content diff ──"
913
+ diff -u "${TARGETS[$tlabel]}/$name/SKILL.md" "$src_path/SKILL.md" 2>/dev/null || true
914
+ echo ""
915
+ fi
916
+ fi
917
+ done
918
+
919
+ [ $has_targets -eq 0 ] && echo "'$name' has no target copies to diff against"
920
+ }
921
+
922
+
923
+ cmd_validate() {
924
+ local name="${1:-}"
925
+ local errors=0
926
+
927
+ if [ -n "$name" ]; then
928
+ local src_path
929
+ src_path=$(source_path_for_skill "$name" 2>/dev/null) || {
930
+ echo "❌ '$name' not found"
931
+ return 1
932
+ }
933
+ _validate_skill "$name" "$src_path"
934
+ return $?
935
+ fi
936
+
937
+ # Validate all
938
+ for n in $(all_skill_names | sort); do
939
+ local sp
940
+ sp=$(source_path_for_skill "$n" 2>/dev/null) || continue
941
+ _validate_skill "$n" "$sp" || errors=$((errors + 1))
942
+ done
943
+ echo ""
944
+ [ $errors -eq 0 ] && echo "All skills valid ✅" || echo "$errors skills have issues"
945
+ }
946
+
947
+ _validate_skill() {
948
+ local name="$1" path="$2"
949
+ local file="$path/SKILL.md"
950
+ local ok=1
951
+
952
+ [ -f "$file" ] || { echo "❌ $name — no SKILL.md"; return 1; }
953
+
954
+ has_frontmatter "$file" || { echo "❌ $name — missing YAML frontmatter (---)"; return 1; }
955
+
956
+ local fm_name fm_desc
957
+ fm_name=$(extract_frontmatter_field "$file" "name")
958
+ fm_desc=$(extract_frontmatter_field "$file" "description")
959
+
960
+ [ -z "$fm_name" ] && { echo "❌ $name — missing 'name' in frontmatter"; ok=0; }
961
+ [ -z "$fm_desc" ] && { echo "❌ $name — missing 'description' in frontmatter"; ok=0; }
962
+
963
+ # Check naming: lowercase, hyphens, digits only
964
+ [[ "$name" =~ ^[a-z0-9][-a-z0-9/]*$ ]] || { echo "⚠️ $name — name should be lowercase-hyphenated"; ok=0; }
965
+
966
+ # Check description length
967
+ if [ -n "$fm_desc" ]; then
968
+ local dlen=${#fm_desc}
969
+ [ $dlen -lt 20 ] && { echo "⚠️ $name — description very short ($dlen chars)"; ok=0; }
970
+ fi
971
+
972
+ [ $ok -eq 1 ] && echo "✅ $name"
973
+ return $(( 1 - ok ))
974
+ }
975
+
976
+ cmd_doctor() {
977
+ echo "Skill system health check"
978
+ echo ""
979
+ local issues=0
980
+
981
+ echo "── Frontmatter & naming ──"
982
+ cmd_validate || issues=$((issues + 1))
983
+
984
+ echo ""
985
+ echo "── Orphaned copies (in targets but no source) ──"
986
+ for tlabel in "${!TARGETS[@]}"; do
987
+ local tdir="${TARGETS[$tlabel]}"
988
+ dir_exists "$tdir" || continue
989
+ for d in "$tdir"/*/; do
990
+ [ -d "$d" ] || continue
991
+ local n
992
+ n=$(basename "$d")
993
+ if [ -f "$d/SKILL.md" ]; then
994
+ if ! source_for_skill "$n" >/dev/null 2>&1; then
995
+ echo " ⚠️ $n — in $tlabel but no canonical source"
996
+ issues=$((issues + 1))
997
+ fi
998
+ else
999
+ for sub in "$d"*/; do
1000
+ [ -d "$sub" ] || continue
1001
+ [ -f "$sub/SKILL.md" ] || continue
1002
+ local sn nested
1003
+ sn=$(basename "$sub")
1004
+ nested="$n/$sn"
1005
+ if ! source_for_skill "$nested" >/dev/null 2>&1; then
1006
+ echo " ⚠️ $nested — in $tlabel but no canonical source"
1007
+ issues=$((issues + 1))
1008
+ fi
1009
+ done
1010
+ fi
1011
+ done
1012
+ done
1013
+ [ $issues -eq 0 ] && echo " None ✅"
1014
+
1015
+ echo ""
1016
+ echo "── Sync drift ──"
1017
+ local drift=0
1018
+ for tlabel in "${!TARGETS[@]}"; do
1019
+ local tdir="${TARGETS[$tlabel]}"
1020
+ dir_exists "$tdir" || continue
1021
+ for d in "$tdir"/*/; do
1022
+ [ -d "$d" ] || continue
1023
+ local n
1024
+ n=$(basename "$d")
1025
+ local src_path
1026
+ src_path=$(source_path_for_skill "$n" 2>/dev/null) || continue
1027
+ if [ "$(md5_content "$src_path")" != "$(md5_content "$d")" ]; then
1028
+ echo " 🔄 $n — drifted in $tlabel"
1029
+ drift=$((drift + 1))
1030
+ fi
1031
+ done
1032
+ done
1033
+ [ $drift -eq 0 ] && echo " All in sync ✅"
1034
+
1035
+ echo ""
1036
+ echo "── Location summary ──"
1037
+ for label in "${SOURCE_PRIORITY[@]}"; do
1038
+ local dir="${SOURCES[$label]}"
1039
+ if dir_exists "$dir"; then
1040
+ local count
1041
+ count=$(find "$dir" -maxdepth 1 -mindepth 1 -type d ! -name ".system" | wc -l)
1042
+ printf " %-12s %2d skills %s\n" "$label" "$count" "$dir"
1043
+ fi
1044
+ done
1045
+ for tlabel in "${!TARGETS[@]}"; do
1046
+ local tdir="${TARGETS[$tlabel]}"
1047
+ if dir_exists "$tdir"; then
1048
+ local count
1049
+ count=$(find "$tdir" -maxdepth 1 -mindepth 1 -type d | wc -l)
1050
+ printf " %-12s %2d skills %s\n" "$tlabel" "$count" "$tdir"
1051
+ fi
1052
+ done
1053
+
1054
+ echo ""
1055
+ local total_issues=$((issues + drift))
1056
+ [ $total_issues -eq 0 ] && echo "All healthy ✅" || echo "$total_issues total issues found"
1057
+ }
1058
+
1059
+
1060
+ # ── Config file writer ─────────────────────────────────────────
1061
+
1062
+ _save_config() {
1063
+ mkdir -p "$CONFIG_DIR"
1064
+ {
1065
+ echo "# Auto-generated by skill-sync."
1066
+ echo "# This file is sourced at startup. Edit manually or use target commands."
1067
+ echo ""
1068
+ declare -p SOURCES
1069
+ declare -p TARGETS
1070
+ declare -p SOURCE_PRIORITY
1071
+ } > "$CONFIG_FILE"
1072
+ }
1073
+
1074
+ # ── New commands ───────────────────────────────────────────────
1075
+
1076
+ cmd_target() {
1077
+ local action="${1:-list}"
1078
+
1079
+ case "$action" in
1080
+ list)
1081
+ echo "Sync targets:"
1082
+ echo ""
1083
+ printf "%-15s %s\n" "LABEL" "PATH"
1084
+ printf "%-15s %s\n" "-----" "----"
1085
+ for tlabel in "${!TARGETS[@]}"; do
1086
+ printf "%-15s %s\n" "$tlabel" "${TARGETS[$tlabel]}"
1087
+ done
1088
+ echo ""
1089
+ echo "Sources:"
1090
+ printf "%-15s %s\n" "LABEL" "PATH"
1091
+ printf "%-15s %s\n" "-----" "----"
1092
+ for slabel in "${SOURCE_PRIORITY[@]}"; do
1093
+ printf "%-15s %s\n" "$slabel" "${SOURCES[$slabel]}"
1094
+ done
1095
+ ;;
1096
+ add)
1097
+ local label="$2" path="$3"
1098
+ [ -z "$label" ] || [ -z "$path" ] && {
1099
+ echo "Usage: skill-sync target add <label> <path>"
1100
+ return 1
1101
+ }
1102
+ # Expand ~ in path
1103
+ path="${path/#\~/$HOME}"
1104
+ [ -n "${TARGETS[$label]:-}" ] && {
1105
+ echo "Target '$label' already exists: ${TARGETS[$label]}"
1106
+ return 1
1107
+ }
1108
+ TARGETS["$label"]="$path"
1109
+ mkdir -p "$path"
1110
+ _save_config
1111
+ echo "✅ Added target '$label' → $path"
1112
+ echo " Config saved to $CONFIG_FILE"
1113
+ ;;
1114
+ remove)
1115
+ local label="$2"
1116
+ [ -z "$label" ] && { echo "Usage: skill-sync target remove <label>"; return 1; }
1117
+ [ -z "${TARGETS[$label]:-}" ] && {
1118
+ echo "Target '$label' not found"
1119
+ return 1
1120
+ }
1121
+ unset "TARGETS[$label]"
1122
+ _save_config
1123
+ echo "🗑️ Removed target '$label'"
1124
+ echo " (skill directories untouched, config updated)"
1125
+ ;;
1126
+ *)
1127
+ echo "Usage: skill-sync target [list|add|remove]"
1128
+ return 1
1129
+ ;;
1130
+ esac
1131
+ }
1132
+
1133
+
1134
+ # ── Main dispatch ──────────────────────────────────────────────
1135
+
1136
+ # Parse global flags
1137
+ local_args=()
1138
+ for a in "$@"; do
1139
+ case "$a" in
1140
+ --dry-run) DRY_RUN=1 ;;
1141
+ *) local_args+=("$a") ;;
1142
+ esac
1143
+ done
1144
+ set -- "${local_args[@]}" 2>/dev/null || true
1145
+ [ $DRY_RUN -eq 1 ] && echo "[dry-run mode — no changes will be made]"
1146
+
1147
+ cmd="${1:-help}"
1148
+ shift || true
1149
+
1150
+ case "$cmd" in
1151
+ sync|"")
1152
+ [ -n "${1:-}" ] && cmd_sync_one "$1" || cmd_sync_all
1153
+ ;;
1154
+ add)
1155
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync add <name> [--to claude]"; exit 1; }
1156
+ cmd_add "$@"
1157
+ ;;
1158
+ remove|rm)
1159
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync remove <name> [--from T] [--purge]"; exit 1; }
1160
+ cmd_remove "$@"
1161
+ ;;
1162
+ create|new)
1163
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync create <name> [--scope agents|opencode]"; exit 1; }
1164
+ cmd_create "$@"
1165
+ ;;
1166
+ edit)
1167
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync edit <name>"; exit 1; }
1168
+ cmd_edit "$1"
1169
+ ;;
1170
+ pull)
1171
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync pull <name> [--from T]"; exit 1; }
1172
+ cmd_pull "$@"
1173
+ ;;
1174
+ search)
1175
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync search <query>"; exit 1; }
1176
+ cmd_search "$@"
1177
+ ;;
1178
+ rename)
1179
+ [ -z "${2:-}" ] && { echo "Usage: skill-sync rename <old> <new>"; exit 1; }
1180
+ cmd_rename "$1" "$2"
1181
+ ;;
1182
+ diff)
1183
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync diff <name>"; exit 1; }
1184
+ cmd_diff "$1"
1185
+ ;;
1186
+ target)
1187
+ cmd_target "${1:-list}" "${2:-}" "${3:-}"
1188
+ ;;
1189
+ info)
1190
+ [ -z "${1:-}" ] && { echo "Usage: skill-sync info <name>"; exit 1; }
1191
+ cmd_info "$1"
1192
+ ;;
1193
+ status)
1194
+ cmd_status
1195
+ ;;
1196
+ list|ls)
1197
+ cmd_list "${1:-}"
1198
+ ;;
1199
+ validate|check)
1200
+ cmd_validate "${1:-}"
1201
+ ;;
1202
+ doctor)
1203
+ cmd_doctor
1204
+ ;;
1205
+ init)
1206
+ cmd_init
1207
+ ;;
1208
+ help|--help|-h)
1209
+ cat << 'HELPEOF'
1210
+ skill-sync — manage skills across all AI agent directories
1211
+
1212
+ Commands:
1213
+ sync [name] Sync all manifest skills, or just one
1214
+ add <name> [--to T] Deploy a skill to a target (default: claude)
1215
+ remove <name> Remove from targets (--from T) or --purge source
1216
+ create <name> [--scope S] Scaffold new skill (default scope: agents)
1217
+ edit <name> Open SKILL.md in $EDITOR, then offer to sync
1218
+ pull <name> [--from T] Reverse sync: copy target edits back to source
1219
+ search <query> Find skills by keyword in name or description
1220
+ rename <old> <new> Rename a skill across all locations
1221
+ diff <name> Show content diff between source and target copies
1222
+ info <name> Show details, sync state, and health
1223
+ status Show sync status for all tracked skills
1224
+ list [--all] List synced skills, or --all for every skill
1225
+ validate [name] Check frontmatter + naming (one or all)
1226
+ doctor Full health check across all locations
1227
+ target [list|add|remove] Manage sync targets dynamically
1228
+ init Generate manifest from current state
1229
+
1230
+ Global flags:
1231
+ --dry-run Preview changes without executing
1232
+
1233
+ Sources (canonical, auto-discovered):
1234
+ agents ~/.agents/skills/
1235
+ opencode ~/.config/opencode/skills/
1236
+ codex ~/.codex/skills/
1237
+
1238
+ Targets (real copies, synced from sources):
1239
+ claude ~/.claude/skills/
1240
+
1241
+ Config: ~/.config/skill-sync/config
1242
+ Manifest: ~/.config/skill-sync/manifest
1243
+ HELPEOF
1244
+ ;;
1245
+ *)
1246
+ echo "Unknown command: $cmd"
1247
+ echo "Run 'skill-sync help' for usage"
1248
+ exit 1
1249
+ ;;
1250
+ esac