@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.
- package/README.md +206 -0
- package/bin/skill-sync.js +38 -0
- package/bin/skill-sync.sh +1250 -0
- package/install.sh +516 -0
- package/lib/authoring.sh +200 -0
- package/lib/core.sh +221 -0
- package/lib/health.sh +136 -0
- package/lib/inspect.sh +210 -0
- package/lib/sync.sh +281 -0
- package/lib/targets.sh +73 -0
- package/package.json +42 -0
- package/rules/skill-sync.md +96 -0
- package/scripts/bundle.sh +28 -0
- package/skill-sync +131 -0
package/lib/authoring.sh
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
cmd_create() {
|
|
2
|
+
local name="$1"
|
|
3
|
+
shift || true
|
|
4
|
+
local scope="agents"
|
|
5
|
+
|
|
6
|
+
while [[ $# -gt 0 ]]; do
|
|
7
|
+
case "$1" in
|
|
8
|
+
--scope) scope="$2"; shift 2 ;;
|
|
9
|
+
--to)
|
|
10
|
+
local to_target="$2"; shift 2
|
|
11
|
+
;;
|
|
12
|
+
*) shift ;;
|
|
13
|
+
esac
|
|
14
|
+
done
|
|
15
|
+
|
|
16
|
+
# Validate scope
|
|
17
|
+
[ -z "${SOURCES[$scope]:-}" ] && {
|
|
18
|
+
echo "Unknown scope '$scope'. Use: agents, opencode, codex"
|
|
19
|
+
return 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
local dir="${SOURCES[$scope]}/$name"
|
|
23
|
+
[ -d "$dir" ] && {
|
|
24
|
+
echo "'$name' already exists at $dir"
|
|
25
|
+
return 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
mkdir -p "$dir"
|
|
29
|
+
cat > "$dir/SKILL.md" << EOF
|
|
30
|
+
---
|
|
31
|
+
name: $name
|
|
32
|
+
description: TODO — describe what this skill does and when to trigger it. Include specific use cases and example phrases the user might say.
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# $(echo "$name" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++)$i=toupper(substr($i,1,1))substr($i,2)}1')
|
|
36
|
+
|
|
37
|
+
## When to Use
|
|
38
|
+
|
|
39
|
+
TODO — Describe the specific scenarios where this skill should be triggered.
|
|
40
|
+
|
|
41
|
+
## Workflow
|
|
42
|
+
|
|
43
|
+
TODO — Describe the step-by-step process.
|
|
44
|
+
|
|
45
|
+
## Key Rules
|
|
46
|
+
|
|
47
|
+
TODO — List any non-negotiable rules or constraints.
|
|
48
|
+
EOF
|
|
49
|
+
|
|
50
|
+
manifest_add "$name" "$scope" "-"
|
|
51
|
+
echo "✅ Created '$name' in $scope"
|
|
52
|
+
echo " $dir/SKILL.md"
|
|
53
|
+
echo ""
|
|
54
|
+
echo " Edit the SKILL.md, then deploy with:"
|
|
55
|
+
echo " skill-sync add $name --to claude"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cmd_edit() {
|
|
59
|
+
local name="$1"
|
|
60
|
+
local src_path
|
|
61
|
+
src_path=$(source_path_for_skill "$name") || {
|
|
62
|
+
echo "'$name' not found in any canonical source"
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
local skill_md="$src_path/SKILL.md"
|
|
67
|
+
[ -f "$skill_md" ] || {
|
|
68
|
+
echo "No SKILL.md at $src_path"
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
local editor="${EDITOR:-vi}"
|
|
73
|
+
echo "Opening $skill_md with $editor..."
|
|
74
|
+
"$editor" "$skill_md"
|
|
75
|
+
|
|
76
|
+
# After editor exits, offer to sync if skill has targets
|
|
77
|
+
local targets
|
|
78
|
+
targets=$(manifest_get_field "$name" "targets" 2>/dev/null)
|
|
79
|
+
if [ -n "$targets" ] && [ "$targets" != "-" ]; then
|
|
80
|
+
echo ""
|
|
81
|
+
echo "Skill '$name' has targets: $targets"
|
|
82
|
+
echo "Run 'skill-sync sync $name' to push changes."
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cmd_pull() {
|
|
87
|
+
local name="$1"
|
|
88
|
+
shift || true
|
|
89
|
+
local from_target=""
|
|
90
|
+
|
|
91
|
+
while [[ $# -gt 0 ]]; do
|
|
92
|
+
case "$1" in
|
|
93
|
+
--from) from_target="$2"; shift 2 ;;
|
|
94
|
+
*) shift ;;
|
|
95
|
+
esac
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
local src_path
|
|
99
|
+
src_path=$(source_path_for_skill "$name") || {
|
|
100
|
+
echo "Canonical source for '$name' not found"
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Find which target to pull from
|
|
105
|
+
local tlabel tdir
|
|
106
|
+
if [ -n "$from_target" ]; then
|
|
107
|
+
tlabel="$from_target"
|
|
108
|
+
tdir="${TARGETS[$tlabel]:-}"
|
|
109
|
+
[ -z "$tdir" ] && { echo "Unknown target '$tlabel'"; return 1; }
|
|
110
|
+
else
|
|
111
|
+
# Auto-detect: find first target that has this skill
|
|
112
|
+
for tl in "${!TARGETS[@]}"; do
|
|
113
|
+
if [ -d "${TARGETS[$tl]}/$name" ]; then
|
|
114
|
+
tlabel="$tl"
|
|
115
|
+
tdir="${TARGETS[$tl]}"
|
|
116
|
+
break
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
[ -z "${tlabel:-}" ] && { echo "'$name' not found in any target"; return 1; }
|
|
122
|
+
|
|
123
|
+
if [ ! -d "$tdir/$name" ]; then
|
|
124
|
+
echo "'$name' not in $tlabel"
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
local src_hash tgt_hash
|
|
129
|
+
src_hash=$(md5_content "$src_path")
|
|
130
|
+
tgt_hash=$(md5_content "$tdir/$name")
|
|
131
|
+
|
|
132
|
+
if [ "$src_hash" = "$tgt_hash" ]; then
|
|
133
|
+
echo "✅ Already in sync — no changes to pull"
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
echo "Pulling '$name' from $tlabel → canonical source..."
|
|
138
|
+
_safe_rm "$src_path"
|
|
139
|
+
_safe_cp "$tdir/$name" "$src_path"
|
|
140
|
+
echo "✅ Pulled. Canonical source updated from $tlabel copy."
|
|
141
|
+
echo " Run 'skill-sync sync' to push to other targets."
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
cmd_rename() {
|
|
145
|
+
local old_name="$1"
|
|
146
|
+
local new_name="$2"
|
|
147
|
+
|
|
148
|
+
[ -z "$new_name" ] && { echo "Usage: skill-sync rename <old> <new>"; return 1; }
|
|
149
|
+
|
|
150
|
+
# Validate new name
|
|
151
|
+
[[ "$new_name" =~ ^[a-z0-9][-a-z0-9/]*$ ]] || {
|
|
152
|
+
echo "Invalid name '$new_name' — use lowercase, hyphens, digits only"
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
local src_label src_path
|
|
157
|
+
src_label=$(source_for_skill "$old_name" 2>/dev/null) || {
|
|
158
|
+
echo "'$old_name' not found in any source"
|
|
159
|
+
return 1
|
|
160
|
+
}
|
|
161
|
+
src_path=$(source_path_for_skill "$old_name")
|
|
162
|
+
|
|
163
|
+
# Check new name doesn't already exist
|
|
164
|
+
if source_for_skill "$new_name" >/dev/null 2>&1; then
|
|
165
|
+
echo "'$new_name' already exists"
|
|
166
|
+
return 1
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
echo "Renaming: $old_name → $new_name"
|
|
170
|
+
echo ""
|
|
171
|
+
|
|
172
|
+
# 1. Rename canonical source
|
|
173
|
+
local src_parent
|
|
174
|
+
src_parent=$(dirname "$src_path")
|
|
175
|
+
_safe_mv "$src_path" "$src_parent/$new_name"
|
|
176
|
+
echo " 📝 Renamed in $src_label source"
|
|
177
|
+
|
|
178
|
+
# 2. Rename in all targets
|
|
179
|
+
for tlabel in "${!TARGETS[@]}"; do
|
|
180
|
+
local tdir="${TARGETS[$tlabel]}"
|
|
181
|
+
if [ -d "$tdir/$old_name" ]; then
|
|
182
|
+
_safe_rm "$tdir/$old_name"
|
|
183
|
+
_safe_cp "$src_parent/$new_name" "$tdir/$new_name"
|
|
184
|
+
echo " 🔄 Updated in $tlabel"
|
|
185
|
+
fi
|
|
186
|
+
done
|
|
187
|
+
|
|
188
|
+
# 3. Update manifest
|
|
189
|
+
manifest_remove "$old_name"
|
|
190
|
+
local targets="-"
|
|
191
|
+
for tlabel in "${!TARGETS[@]}"; do
|
|
192
|
+
if [ -d "${TARGETS[$tlabel]}/$new_name" ]; then
|
|
193
|
+
[ "$targets" = "-" ] && targets="$tlabel" || targets="$targets,$tlabel"
|
|
194
|
+
fi
|
|
195
|
+
done
|
|
196
|
+
manifest_add "$new_name" "$src_label" "$targets"
|
|
197
|
+
|
|
198
|
+
echo ""
|
|
199
|
+
echo "✅ Renamed $old_name → $new_name across all locations"
|
|
200
|
+
}
|
package/lib/core.sh
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -- Configuration --
|
|
2
|
+
|
|
3
|
+
CONFIG_DIR="${SKILLSYNC_CONFIG_DIR:-$HOME/.config/skill-sync}"
|
|
4
|
+
MANIFEST="$CONFIG_DIR/manifest"
|
|
5
|
+
CONFIG_FILE="$CONFIG_DIR/config"
|
|
6
|
+
|
|
7
|
+
# Defaults (overridden by config file if present)
|
|
8
|
+
declare -A SOURCES=(
|
|
9
|
+
[agents]="${SKILLSYNC_AGENTS_DIR:-$HOME/.agents/skills}"
|
|
10
|
+
[opencode]="${SKILLSYNC_OPENCODE_DIR:-$HOME/.config/opencode/skills}"
|
|
11
|
+
[codex]="${SKILLSYNC_CODEX_DIR:-$HOME/.codex/skills}"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
declare -A TARGETS=(
|
|
15
|
+
[claude]="${SKILLSYNC_CLAUDE_DIR:-$HOME/.claude/skills}"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
SOURCE_PRIORITY=("agents" "opencode" "codex")
|
|
19
|
+
|
|
20
|
+
# Load config file if it exists (overrides defaults)
|
|
21
|
+
[ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE"
|
|
22
|
+
|
|
23
|
+
# Global flags
|
|
24
|
+
DRY_RUN=0
|
|
25
|
+
|
|
26
|
+
# -- Helpers --
|
|
27
|
+
|
|
28
|
+
# Safe operations that respect --dry-run
|
|
29
|
+
_safe_cp() { [ $DRY_RUN -eq 1 ] && echo " [dry-run] cp -r $1 $2" || cp -r "$1" "$2"; }
|
|
30
|
+
_safe_rm() { [ $DRY_RUN -eq 1 ] && echo " [dry-run] rm -rf $1" || rm -rf "$1"; }
|
|
31
|
+
_safe_mv() { [ $DRY_RUN -eq 1 ] && echo " [dry-run] mv $1 $2" || mv "$1" "$2"; }
|
|
32
|
+
|
|
33
|
+
md5_content() {
|
|
34
|
+
find "$1" -type f -exec md5sum {} \; 2>/dev/null | awk '{print $1}' | sort | md5sum | cut -d' ' -f1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dir_exists() { [ -d "$1" ]; }
|
|
38
|
+
|
|
39
|
+
source_for_skill() {
|
|
40
|
+
local name="$1"
|
|
41
|
+
for label in "${SOURCE_PRIORITY[@]}"; do
|
|
42
|
+
local dir="${SOURCES[$label]}"
|
|
43
|
+
if [ -f "$dir/$name/SKILL.md" ] || [ -d "$dir/$name" ]; then
|
|
44
|
+
echo "$label"
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
source_path_for_skill() {
|
|
52
|
+
local name="$1"
|
|
53
|
+
for label in "${SOURCE_PRIORITY[@]}"; do
|
|
54
|
+
local dir="${SOURCES[$label]}"
|
|
55
|
+
if [ -f "$dir/$name/SKILL.md" ] || [ -d "$dir/$name" ]; then
|
|
56
|
+
echo "$dir/$name"
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
all_skill_names() {
|
|
64
|
+
declare -A seen
|
|
65
|
+
for label in "${SOURCE_PRIORITY[@]}"; do
|
|
66
|
+
local dir="${SOURCES[$label]}"
|
|
67
|
+
dir_exists "$dir" || continue
|
|
68
|
+
for d in "$dir"/*/; do
|
|
69
|
+
[ -d "$d" ] || continue
|
|
70
|
+
local n
|
|
71
|
+
n=$(basename "$d")
|
|
72
|
+
[ "$n" = ".system" ] && continue
|
|
73
|
+
if [ -f "$d/SKILL.md" ]; then
|
|
74
|
+
[ -n "${seen[$n]:-}" ] && continue
|
|
75
|
+
seen[$n]=1
|
|
76
|
+
echo "$n"
|
|
77
|
+
else
|
|
78
|
+
for sub in "$d"*/; do
|
|
79
|
+
[ -d "$sub" ] || continue
|
|
80
|
+
[ -f "$sub/SKILL.md" ] || continue
|
|
81
|
+
local sn nested
|
|
82
|
+
sn=$(basename "$sub")
|
|
83
|
+
nested="$n/$sn"
|
|
84
|
+
[ -n "${seen[$nested]:-}" ] && continue
|
|
85
|
+
seen[$nested]=1
|
|
86
|
+
echo "$nested"
|
|
87
|
+
done
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
done
|
|
91
|
+
for label in "${!TARGETS[@]}"; do
|
|
92
|
+
local dir="${TARGETS[$label]}"
|
|
93
|
+
dir_exists "$dir" || continue
|
|
94
|
+
for d in "$dir"/*/; do
|
|
95
|
+
[ -d "$d" ] || continue
|
|
96
|
+
local n
|
|
97
|
+
n=$(basename "$d")
|
|
98
|
+
if [ -f "$d/SKILL.md" ]; then
|
|
99
|
+
[ -n "${seen[$n]:-}" ] && continue
|
|
100
|
+
seen[$n]=1
|
|
101
|
+
echo "$n"
|
|
102
|
+
else
|
|
103
|
+
for sub in "$d"*/; do
|
|
104
|
+
[ -d "$sub" ] || continue
|
|
105
|
+
[ -f "$sub/SKILL.md" ] || continue
|
|
106
|
+
local sn nested
|
|
107
|
+
sn=$(basename "$sub")
|
|
108
|
+
nested="$n/$sn"
|
|
109
|
+
[ -n "${seen[$nested]:-}" ] && continue
|
|
110
|
+
seen[$nested]=1
|
|
111
|
+
echo "$nested"
|
|
112
|
+
done
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
done
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
extract_frontmatter_field() {
|
|
119
|
+
local file="$1"
|
|
120
|
+
local field="$2"
|
|
121
|
+
[ -f "$file" ] || return 1
|
|
122
|
+
awk -v field="$field" '
|
|
123
|
+
BEGIN { found=0; block=""; is_block=0 }
|
|
124
|
+
/^---$/ {
|
|
125
|
+
if (found && is_block) { print block; exit }
|
|
126
|
+
in_fm = !in_fm; next
|
|
127
|
+
}
|
|
128
|
+
in_fm && !found && $0 ~ "^"field":" {
|
|
129
|
+
val = $0
|
|
130
|
+
sub("^"field":\\s*", "", val)
|
|
131
|
+
if (val == ">" || val == ">-" || val == "|" || val == "|-") {
|
|
132
|
+
is_block = 1; found = 1; next
|
|
133
|
+
}
|
|
134
|
+
gsub(/^["'\'']|["'\'']$/, "", val)
|
|
135
|
+
if (val == ">") val = ""
|
|
136
|
+
print val
|
|
137
|
+
exit
|
|
138
|
+
}
|
|
139
|
+
found && is_block && /^[ \t]/ {
|
|
140
|
+
line = $0; sub(/^[ \t]+/, "", line)
|
|
141
|
+
if (block == "") block = line
|
|
142
|
+
else block = block " " line
|
|
143
|
+
next
|
|
144
|
+
}
|
|
145
|
+
found && is_block && /^[^ \t]/ { print block; exit }
|
|
146
|
+
END { if (found && is_block) print block }
|
|
147
|
+
' "$file"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
has_frontmatter() {
|
|
151
|
+
local file="$1"
|
|
152
|
+
[ -f "$file" ] || return 1
|
|
153
|
+
head -1 "$file" 2>/dev/null | grep -q '^---$'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# -- Manifest --
|
|
157
|
+
|
|
158
|
+
ensure_manifest() {
|
|
159
|
+
mkdir -p "$CONFIG_DIR"
|
|
160
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
161
|
+
cmd_init
|
|
162
|
+
fi
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
manifest_get_field() {
|
|
166
|
+
local name="$1" field="$2"
|
|
167
|
+
[ -f "$MANIFEST" ] || return 1
|
|
168
|
+
awk -F'|' -v name="$name" -v field="$field" '
|
|
169
|
+
NR <= 4 { next }
|
|
170
|
+
{ gsub(/[ \t]+/, "", $1); gsub(/[ \t]+/, "", $2); gsub(/[ \t]+/, "", $3) }
|
|
171
|
+
$1 == name {
|
|
172
|
+
if (field == "source") print $2
|
|
173
|
+
else if (field == "targets") print $3
|
|
174
|
+
exit
|
|
175
|
+
}
|
|
176
|
+
' "$MANIFEST"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
manifest_has() {
|
|
180
|
+
local name="$1"
|
|
181
|
+
[ -f "$MANIFEST" ] || return 1
|
|
182
|
+
awk -F'|' -v name="$name" '
|
|
183
|
+
NR <= 4 { next }
|
|
184
|
+
{ gsub(/[ \t]+/, "", $1) }
|
|
185
|
+
$1 == name { found=1; exit }
|
|
186
|
+
END { exit !found }
|
|
187
|
+
' "$MANIFEST"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
manifest_add() {
|
|
191
|
+
local name="$1" source="$2" targets="$3"
|
|
192
|
+
ensure_manifest
|
|
193
|
+
if manifest_has "$name"; then
|
|
194
|
+
local tmp
|
|
195
|
+
tmp=$(mktemp)
|
|
196
|
+
awk -F'|' -v name="$name" -v src="$source" -v tgt="$targets" '
|
|
197
|
+
NR <= 4 { print; next }
|
|
198
|
+
{ gsub(/[ \t]+/, "", $1) }
|
|
199
|
+
$1 == name { printf "%-30s | %-10s | %s\n", name, src, tgt; next }
|
|
200
|
+
{ print }
|
|
201
|
+
' "$MANIFEST" > "$tmp"
|
|
202
|
+
mv "$tmp" "$MANIFEST"
|
|
203
|
+
else
|
|
204
|
+
printf "%-30s | %-10s | %s\n" "$name" "$source" "$targets" >> "$MANIFEST"
|
|
205
|
+
fi
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
manifest_remove() {
|
|
209
|
+
local name="$1"
|
|
210
|
+
[ -f "$MANIFEST" ] || return 0
|
|
211
|
+
local tmp
|
|
212
|
+
tmp=$(mktemp)
|
|
213
|
+
awk -F'|' -v name="$name" '
|
|
214
|
+
NR <= 4 { print; next }
|
|
215
|
+
{ gsub(/[ \t]+/, "", $1) }
|
|
216
|
+
$1 != name { print }
|
|
217
|
+
' "$MANIFEST" > "$tmp"
|
|
218
|
+
mv "$tmp" "$MANIFEST"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
package/lib/health.sh
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
cmd_validate() {
|
|
2
|
+
local name="${1:-}"
|
|
3
|
+
local errors=0
|
|
4
|
+
|
|
5
|
+
if [ -n "$name" ]; then
|
|
6
|
+
local src_path
|
|
7
|
+
src_path=$(source_path_for_skill "$name" 2>/dev/null) || {
|
|
8
|
+
echo "❌ '$name' not found"
|
|
9
|
+
return 1
|
|
10
|
+
}
|
|
11
|
+
_validate_skill "$name" "$src_path"
|
|
12
|
+
return $?
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Validate all
|
|
16
|
+
for n in $(all_skill_names | sort); do
|
|
17
|
+
local sp
|
|
18
|
+
sp=$(source_path_for_skill "$n" 2>/dev/null) || continue
|
|
19
|
+
_validate_skill "$n" "$sp" || errors=$((errors + 1))
|
|
20
|
+
done
|
|
21
|
+
echo ""
|
|
22
|
+
[ $errors -eq 0 ] && echo "All skills valid ✅" || echo "$errors skills have issues"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_validate_skill() {
|
|
26
|
+
local name="$1" path="$2"
|
|
27
|
+
local file="$path/SKILL.md"
|
|
28
|
+
local ok=1
|
|
29
|
+
|
|
30
|
+
[ -f "$file" ] || { echo "❌ $name — no SKILL.md"; return 1; }
|
|
31
|
+
|
|
32
|
+
has_frontmatter "$file" || { echo "❌ $name — missing YAML frontmatter (---)"; return 1; }
|
|
33
|
+
|
|
34
|
+
local fm_name fm_desc
|
|
35
|
+
fm_name=$(extract_frontmatter_field "$file" "name")
|
|
36
|
+
fm_desc=$(extract_frontmatter_field "$file" "description")
|
|
37
|
+
|
|
38
|
+
[ -z "$fm_name" ] && { echo "❌ $name — missing 'name' in frontmatter"; ok=0; }
|
|
39
|
+
[ -z "$fm_desc" ] && { echo "❌ $name — missing 'description' in frontmatter"; ok=0; }
|
|
40
|
+
|
|
41
|
+
# Check naming: lowercase, hyphens, digits only
|
|
42
|
+
[[ "$name" =~ ^[a-z0-9][-a-z0-9/]*$ ]] || { echo "⚠️ $name — name should be lowercase-hyphenated"; ok=0; }
|
|
43
|
+
|
|
44
|
+
# Check description length
|
|
45
|
+
if [ -n "$fm_desc" ]; then
|
|
46
|
+
local dlen=${#fm_desc}
|
|
47
|
+
[ $dlen -lt 20 ] && { echo "⚠️ $name — description very short ($dlen chars)"; ok=0; }
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
[ $ok -eq 1 ] && echo "✅ $name"
|
|
51
|
+
return $(( 1 - ok ))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
cmd_doctor() {
|
|
55
|
+
echo "Skill system health check"
|
|
56
|
+
echo ""
|
|
57
|
+
local issues=0
|
|
58
|
+
|
|
59
|
+
echo "── Frontmatter & naming ──"
|
|
60
|
+
cmd_validate || issues=$((issues + 1))
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo "── Orphaned copies (in targets but no source) ──"
|
|
64
|
+
for tlabel in "${!TARGETS[@]}"; do
|
|
65
|
+
local tdir="${TARGETS[$tlabel]}"
|
|
66
|
+
dir_exists "$tdir" || continue
|
|
67
|
+
for d in "$tdir"/*/; do
|
|
68
|
+
[ -d "$d" ] || continue
|
|
69
|
+
local n
|
|
70
|
+
n=$(basename "$d")
|
|
71
|
+
if [ -f "$d/SKILL.md" ]; then
|
|
72
|
+
if ! source_for_skill "$n" >/dev/null 2>&1; then
|
|
73
|
+
echo " ⚠️ $n — in $tlabel but no canonical source"
|
|
74
|
+
issues=$((issues + 1))
|
|
75
|
+
fi
|
|
76
|
+
else
|
|
77
|
+
for sub in "$d"*/; do
|
|
78
|
+
[ -d "$sub" ] || continue
|
|
79
|
+
[ -f "$sub/SKILL.md" ] || continue
|
|
80
|
+
local sn nested
|
|
81
|
+
sn=$(basename "$sub")
|
|
82
|
+
nested="$n/$sn"
|
|
83
|
+
if ! source_for_skill "$nested" >/dev/null 2>&1; then
|
|
84
|
+
echo " ⚠️ $nested — in $tlabel but no canonical source"
|
|
85
|
+
issues=$((issues + 1))
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
done
|
|
91
|
+
[ $issues -eq 0 ] && echo " None ✅"
|
|
92
|
+
|
|
93
|
+
echo ""
|
|
94
|
+
echo "── Sync drift ──"
|
|
95
|
+
local drift=0
|
|
96
|
+
for tlabel in "${!TARGETS[@]}"; do
|
|
97
|
+
local tdir="${TARGETS[$tlabel]}"
|
|
98
|
+
dir_exists "$tdir" || continue
|
|
99
|
+
for d in "$tdir"/*/; do
|
|
100
|
+
[ -d "$d" ] || continue
|
|
101
|
+
local n
|
|
102
|
+
n=$(basename "$d")
|
|
103
|
+
local src_path
|
|
104
|
+
src_path=$(source_path_for_skill "$n" 2>/dev/null) || continue
|
|
105
|
+
if [ "$(md5_content "$src_path")" != "$(md5_content "$d")" ]; then
|
|
106
|
+
echo " 🔄 $n — drifted in $tlabel"
|
|
107
|
+
drift=$((drift + 1))
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
done
|
|
111
|
+
[ $drift -eq 0 ] && echo " All in sync ✅"
|
|
112
|
+
|
|
113
|
+
echo ""
|
|
114
|
+
echo "── Location summary ──"
|
|
115
|
+
for label in "${SOURCE_PRIORITY[@]}"; do
|
|
116
|
+
local dir="${SOURCES[$label]}"
|
|
117
|
+
if dir_exists "$dir"; then
|
|
118
|
+
local count
|
|
119
|
+
count=$(find "$dir" -maxdepth 1 -mindepth 1 -type d ! -name ".system" | wc -l)
|
|
120
|
+
printf " %-12s %2d skills %s\n" "$label" "$count" "$dir"
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
for tlabel in "${!TARGETS[@]}"; do
|
|
124
|
+
local tdir="${TARGETS[$tlabel]}"
|
|
125
|
+
if dir_exists "$tdir"; then
|
|
126
|
+
local count
|
|
127
|
+
count=$(find "$tdir" -maxdepth 1 -mindepth 1 -type d | wc -l)
|
|
128
|
+
printf " %-12s %2d skills %s\n" "$tlabel" "$count" "$tdir"
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
echo ""
|
|
133
|
+
local total_issues=$((issues + drift))
|
|
134
|
+
[ $total_issues -eq 0 ] && echo "All healthy ✅" || echo "$total_issues total issues found"
|
|
135
|
+
}
|
|
136
|
+
|