@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,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
+