@laitszkin/apollo-toolkit 3.2.2 → 3.3.1

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.
@@ -4,7 +4,8 @@ set -euo pipefail
4
4
  usage() {
5
5
  cat <<"USAGE"
6
6
  Usage:
7
- ./scripts/install_skills.sh [codex|openclaw|trae|agents|claude-code|all]...
7
+ ./scripts/install_skills.sh [install] [codex|openclaw|trae|agents|claude-code|all]...
8
+ ./scripts/install_skills.sh uninstall [codex|openclaw|trae|agents|claude-code|all]...
8
9
 
9
10
  Modes:
10
11
  codex Copy skills into ~/.codex/skills (includes ./codex/ agent-specific skills)
@@ -14,6 +15,10 @@ Modes:
14
15
  claude-code Copy skills into ~/.claude/skills
15
16
  all Install all supported targets
16
17
 
18
+ Options:
19
+ --symlink Install skills as symlinks (recommended; auto-update via git pull)
20
+ --copy Install skills as file copies (manual reinstall for updates)
21
+
17
22
  Optional environment overrides:
18
23
  CODEX_SKILLS_DIR Override codex skills destination path
19
24
  OPENCLAW_HOME Override openclaw home path
@@ -27,6 +32,8 @@ USAGE
27
32
 
28
33
  SCRIPT_SOURCE="${BASH_SOURCE[0]-}"
29
34
  TOOLKIT_REPO_URL="${APOLLO_TOOLKIT_REPO_URL:-https://github.com/LaiTszKin/apollo-toolkit.git}"
35
+ MANIFEST_FILENAME=".apollo-toolkit-manifest.json"
36
+ LINK_MODE=""
30
37
 
31
38
  expand_user_path() {
32
39
  local raw_path="${1-}"
@@ -77,10 +84,14 @@ else
77
84
  fi
78
85
  SCRIPT_DIR="$REPO_ROOT/scripts"
79
86
  fi
87
+
88
+ # ---- State variables ----
80
89
  SELECTED_MODES=()
81
90
  SHARED_SKILL_PATHS=()
82
91
  CODEX_SKILL_PATHS=()
83
92
 
93
+ # ---- Skill collection ----
94
+
84
95
  collect_skills() {
85
96
  local dir
86
97
  SHARED_SKILL_PATHS=()
@@ -91,7 +102,6 @@ collect_skills() {
91
102
  fi
92
103
  done < <(find "$REPO_ROOT" -mindepth 1 -maxdepth 1 -type d | sort)
93
104
 
94
- # For codex mode, also include codex-specific skills
95
105
  if [[ " ${SELECTED_MODES[*]} " =~ " codex " ]]; then
96
106
  local codex_dir="$REPO_ROOT/codex"
97
107
  if [[ -d "$codex_dir" ]]; then
@@ -109,6 +119,150 @@ collect_skills() {
109
119
  fi
110
120
  }
111
121
 
122
+ # Get skill names from paths (basename only, deduplicated)
123
+ get_skill_names() {
124
+ local -a paths=("$@")
125
+ local -a names=()
126
+ local name
127
+ for path in "${paths[@]}"; do
128
+ name="$(basename "$path")"
129
+ names+=("$name")
130
+ done
131
+ printf '%s\n' "${names[@]}" | sort -u
132
+ }
133
+
134
+ # ---- Manifest management ----
135
+
136
+ read_manifest_skills() {
137
+ local target_root="$1"
138
+ local manifest_file="$target_root/$MANIFEST_FILENAME"
139
+ if [[ -f "$manifest_file" ]]; then
140
+ # Extract skill names from JSON manifest (historical + current, deduplicated)
141
+ # Use python3 if available, otherwise fall back to simple grep
142
+ if command -v python3 >/dev/null 2>&1; then
143
+ python3 -c "
144
+ import json, sys
145
+ try:
146
+ with open('$manifest_file') as f:
147
+ m = json.load(f)
148
+ skills = set(m.get('historicalSkills', []) + m.get('skills', []))
149
+ for s in sorted(skills):
150
+ print(s)
151
+ except: pass
152
+ " 2>/dev/null || true
153
+ else
154
+ # Fallback: grep for skill names in JSON array
155
+ grep -E '^\s*"' "$manifest_file" 2>/dev/null | \
156
+ sed 's/.*"\([^"]*\)".*/\1/' | \
157
+ grep -v 'version\|installedAt\|linkMode\|skills\|historicalSkills\|source' | \
158
+ sort -u || true
159
+ fi
160
+ fi
161
+ }
162
+
163
+ write_manifest() {
164
+ local target_root="$1"
165
+ local version="${2:-unknown}"
166
+ local link_mode="$3"
167
+ shift 3
168
+ local -a skill_names=("$@")
169
+
170
+ local manifest_file="$target_root/$MANIFEST_FILENAME"
171
+
172
+ # Read existing manifest for historical skills
173
+ local -a historical_skills=()
174
+ if [[ -f "$manifest_file" ]]; then
175
+ while IFS= read -r name; do
176
+ [[ -n "$name" ]] && historical_skills+=("$name")
177
+ done < <(read_manifest_skills "$target_root")
178
+ fi
179
+
180
+ # Merge current + historical, deduplicate
181
+ local -a merged=()
182
+ local name
183
+ for name in "${historical_skills[@]}" "${skill_names[@]}"; do
184
+ merged+=("$name")
185
+ done
186
+
187
+ local -a all_skills_sorted
188
+ while IFS= read -r name; do
189
+ [[ -n "$name" ]] && all_skills_sorted+=("$name")
190
+ done < <(printf '%s\n' "${merged[@]}" | sort -u)
191
+
192
+ local now
193
+ now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
194
+
195
+ # Write JSON manifest manually (no jq dependency)
196
+ mkdir -p "$target_root"
197
+ {
198
+ printf '{\n'
199
+ printf ' "version": "%s",\n' "$version"
200
+ printf ' "installedAt": "%s",\n' "$now"
201
+ printf ' "linkMode": "%s",\n' "$link_mode"
202
+ printf ' "skills": [\n'
203
+ local i=0
204
+ for name in "${skill_names[@]}"; do
205
+ if [[ $i -gt 0 ]]; then printf ',\n'; fi
206
+ printf ' "%s"' "$name"
207
+ i=$((i + 1))
208
+ done
209
+ printf '\n ],\n'
210
+ printf ' "historicalSkills": [\n'
211
+ i=0
212
+ for name in "${all_skills_sorted[@]}"; do
213
+ if [[ $i -gt 0 ]]; then printf ',\n'; fi
214
+ printf ' "%s"' "$name"
215
+ i=$((i + 1))
216
+ done
217
+ printf '\n ]\n'
218
+ printf '}\n'
219
+ } > "$manifest_file"
220
+ }
221
+
222
+ # List all known skill names (current + historical from all manifests, deduplicated)
223
+ list_all_known_skill_names() {
224
+ local -a target_dirs=()
225
+ local target_dir
226
+
227
+ # Collect all potential target directories
228
+ for mode in "${SELECTED_MODES[@]}"; do
229
+ case "$mode" in
230
+ codex) target_dirs+=("$(expand_user_path "${CODEX_SKILLS_DIR:-$HOME/.codex/skills}")") ;;
231
+ trae) target_dirs+=("$(expand_user_path "${TRAE_SKILLS_DIR:-$HOME/.trae/skills}")") ;;
232
+ agents) target_dirs+=("$(expand_user_path "${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}")") ;;
233
+ claude-code) target_dirs+=("$(expand_user_path "${CLAUDE_CODE_SKILLS_DIR:-$HOME/.claude/skills}")") ;;
234
+ openclaw)
235
+ local openclaw_home oc_workspace
236
+ openclaw_home="$(expand_user_path "${OPENCLAW_HOME:-$HOME/.openclaw}")"
237
+ if [[ -d "$openclaw_home" ]]; then
238
+ for oc_workspace in "$openclaw_home"/workspace*; do
239
+ [[ -d "$oc_workspace/skills" ]] && target_dirs+=("$oc_workspace/skills")
240
+ done
241
+ fi
242
+ ;;
243
+ esac
244
+ done
245
+
246
+ local -a all_names=()
247
+
248
+ # Current skill names from repo
249
+ local name
250
+ while IFS= read -r name; do
251
+ [[ -n "$name" ]] && all_names+=("$name")
252
+ done < <(get_skill_names "${SHARED_SKILL_PATHS[@]}" "${CODEX_SKILL_PATHS[@]}")
253
+
254
+ # Historical from manifests
255
+ for target_dir in "${target_dirs[@]}"; do
256
+ while IFS= read -r name; do
257
+ [[ -n "$name" ]] && all_names+=("$name")
258
+ done < <(read_manifest_skills "$target_dir")
259
+ done
260
+
261
+ printf '%s\n' "${all_names[@]}" | sort -u
262
+ }
263
+
264
+ # ---- Install operations ----
265
+
112
266
  replace_with_copy() {
113
267
  local src="$1"
114
268
  local target_root="$2"
@@ -125,14 +279,41 @@ replace_with_copy() {
125
279
  echo "[copied] $src -> $target"
126
280
  }
127
281
 
282
+ replace_with_symlink() {
283
+ local src="$1"
284
+ local target_root="$2"
285
+ local name target
286
+
287
+ name="$(basename "$src")"
288
+ target="$target_root/$name"
289
+
290
+ mkdir -p "$target_root"
291
+ if [[ -e "$target" || -L "$target" ]]; then
292
+ rm -rf "$target"
293
+ fi
294
+ ln -s "$src" "$target"
295
+ echo "[symlink] $src -> $target"
296
+ }
297
+
298
+ do_replace() {
299
+ if [[ "$LINK_MODE" == "symlink" ]]; then
300
+ replace_with_symlink "$@"
301
+ else
302
+ replace_with_copy "$@"
303
+ fi
304
+ }
305
+
128
306
  install_codex() {
129
307
  local codex_skills_dir src
130
308
  codex_skills_dir="$(expand_user_path "${CODEX_SKILLS_DIR:-$HOME/.codex/skills}")"
131
309
 
132
- echo "Installing to codex: $codex_skills_dir"
310
+ echo "Installing to codex: $codex_skills_dir (mode: $LINK_MODE)"
311
+ local -a skill_names=()
133
312
  for src in "${SHARED_SKILL_PATHS[@]}" "${CODEX_SKILL_PATHS[@]}"; do
134
- replace_with_copy "$src" "$codex_skills_dir"
313
+ do_replace "$src" "$codex_skills_dir"
314
+ skill_names+=("$(basename "$src")")
135
315
  done
316
+ write_manifest "$codex_skills_dir" "${VERSION:-unknown}" "$LINK_MODE" "${skill_names[@]}"
136
317
  }
137
318
 
138
319
  install_openclaw() {
@@ -153,10 +334,13 @@ install_openclaw() {
153
334
 
154
335
  for workspace in "${workspaces[@]}"; do
155
336
  skills_dir="$workspace/skills"
156
- echo "Installing to openclaw workspace: $skills_dir"
337
+ echo "Installing to openclaw workspace: $skills_dir (mode: $LINK_MODE)"
338
+ local -a skill_names=()
157
339
  for src in "${SHARED_SKILL_PATHS[@]}"; do
158
- replace_with_copy "$src" "$skills_dir"
340
+ do_replace "$src" "$skills_dir"
341
+ skill_names+=("$(basename "$src")")
159
342
  done
343
+ write_manifest "$skills_dir" "${VERSION:-unknown}" "$LINK_MODE" "${skill_names[@]}"
160
344
  done
161
345
  }
162
346
 
@@ -164,32 +348,133 @@ install_trae() {
164
348
  local trae_skills_dir src
165
349
  trae_skills_dir="$(expand_user_path "${TRAE_SKILLS_DIR:-$HOME/.trae/skills}")"
166
350
 
167
- echo "Installing to trae: $trae_skills_dir"
351
+ echo "Installing to trae: $trae_skills_dir (mode: $LINK_MODE)"
352
+ local -a skill_names=()
168
353
  for src in "${SHARED_SKILL_PATHS[@]}"; do
169
- replace_with_copy "$src" "$trae_skills_dir"
354
+ do_replace "$src" "$trae_skills_dir"
355
+ skill_names+=("$(basename "$src")")
170
356
  done
357
+ write_manifest "$trae_skills_dir" "${VERSION:-unknown}" "$LINK_MODE" "${skill_names[@]}"
171
358
  }
172
359
 
173
360
  install_agents() {
174
361
  local agents_skills_dir src
175
362
  agents_skills_dir="$(expand_user_path "${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}")"
176
363
 
177
- echo "Installing to agents: $agents_skills_dir"
364
+ echo "Installing to agents: $agents_skills_dir (mode: $LINK_MODE)"
365
+ local -a skill_names=()
178
366
  for src in "${SHARED_SKILL_PATHS[@]}"; do
179
- replace_with_copy "$src" "$agents_skills_dir"
367
+ do_replace "$src" "$agents_skills_dir"
368
+ skill_names+=("$(basename "$src")")
180
369
  done
370
+ write_manifest "$agents_skills_dir" "${VERSION:-unknown}" "$LINK_MODE" "${skill_names[@]}"
181
371
  }
182
372
 
183
373
  install_claude_code() {
184
374
  local claude_code_skills_dir src
185
375
  claude_code_skills_dir="$(expand_user_path "${CLAUDE_CODE_SKILLS_DIR:-$HOME/.claude/skills}")"
186
376
 
187
- echo "Installing to claude-code: $claude_code_skills_dir"
377
+ echo "Installing to claude-code: $claude_code_skills_dir (mode: $LINK_MODE)"
378
+ local -a skill_names=()
188
379
  for src in "${SHARED_SKILL_PATHS[@]}"; do
189
- replace_with_copy "$src" "$claude_code_skills_dir"
380
+ do_replace "$src" "$claude_code_skills_dir"
381
+ skill_names+=("$(basename "$src")")
190
382
  done
383
+ write_manifest "$claude_code_skills_dir" "${VERSION:-unknown}" "$LINK_MODE" "${skill_names[@]}"
191
384
  }
192
385
 
386
+ # ---- Uninstall operations ----
387
+
388
+ uninstall_target() {
389
+ local target_root="$1"
390
+ local target_label="$2"
391
+
392
+ local manifest_file="$target_root/$MANIFEST_FILENAME"
393
+ if [[ ! -f "$manifest_file" ]]; then
394
+ echo "[skip] No manifest found in: $target_root" >&2
395
+ return
396
+ fi
397
+
398
+ local -a skills=()
399
+ local name
400
+ while IFS= read -r name; do
401
+ [[ -n "$name" ]] && skills+=("$name")
402
+ done < <(read_manifest_skills "$target_root")
403
+
404
+ if [[ ${#skills[@]} -eq 0 ]]; then
405
+ echo "[skip] No skills in manifest: $target_root" >&2
406
+ rm -f "$manifest_file"
407
+ return
408
+ fi
409
+
410
+ echo "Uninstalling from $target_label: $target_root"
411
+ for name in "${skills[@]}"; do
412
+ local skill_path="$target_root/$name"
413
+ if [[ -e "$skill_path" || -L "$skill_path" ]]; then
414
+ rm -rf "$skill_path"
415
+ echo " [removed] $skill_path"
416
+ fi
417
+ done
418
+
419
+ rm -f "$manifest_file"
420
+ echo " [removed manifest] $manifest_file"
421
+ }
422
+
423
+ run_uninstall() {
424
+ local mode
425
+
426
+ if [[ ${#SELECTED_MODES[@]} -eq 0 ]]; then
427
+ # Uninstall from all known targets
428
+ SELECTED_MODES=(codex openclaw trae agents claude-code)
429
+ fi
430
+
431
+ echo "Uninstalling Apollo Toolkit skills..."
432
+ echo "Target modes: ${SELECTED_MODES[*]}"
433
+ echo
434
+
435
+ # Show all known skills (current + historical, deduplicated)
436
+ collect_skills
437
+ echo "All known skills (current + historical):"
438
+ list_all_known_skill_names | while read -r name; do
439
+ [[ -n "$name" ]] && echo " - $name"
440
+ done
441
+ echo
442
+
443
+ for mode in "${SELECTED_MODES[@]}"; do
444
+ case "$mode" in
445
+ codex)
446
+ local dir="$(expand_user_path "${CODEX_SKILLS_DIR:-$HOME/.codex/skills}")"
447
+ uninstall_target "$dir" "codex"
448
+ ;;
449
+ openclaw)
450
+ local openclaw_home oc_workspace
451
+ openclaw_home="$(expand_user_path "${OPENCLAW_HOME:-$HOME/.openclaw}")"
452
+ if [[ -d "$openclaw_home" ]]; then
453
+ for oc_workspace in "$openclaw_home"/workspace*; do
454
+ uninstall_target "$oc_workspace/skills" "openclaw"
455
+ done
456
+ fi
457
+ ;;
458
+ trae)
459
+ local dir="$(expand_user_path "${TRAE_SKILLS_DIR:-$HOME/.trae/skills}")"
460
+ uninstall_target "$dir" "trae"
461
+ ;;
462
+ agents)
463
+ local dir="$(expand_user_path "${AGENTS_SKILLS_DIR:-$HOME/.agents/skills}")"
464
+ uninstall_target "$dir" "agents"
465
+ ;;
466
+ claude-code)
467
+ local dir="$(expand_user_path "${CLAUDE_CODE_SKILLS_DIR:-$HOME/.claude/skills}")"
468
+ uninstall_target "$dir" "claude-code"
469
+ ;;
470
+ esac
471
+ done
472
+
473
+ echo "Done."
474
+ }
475
+
476
+ # ---- Mode management ----
477
+
193
478
  add_mode_once() {
194
479
  local mode="$1"
195
480
  local existing
@@ -242,6 +527,89 @@ read_choice_from_user() {
242
527
  printf '%s' "$result"
243
528
  }
244
529
 
530
+ read_yes_no() {
531
+ local prompt="$1"
532
+ local default_yes="${2:-true}"
533
+ local hint result
534
+
535
+ if [[ "$default_yes" == "true" ]]; then
536
+ hint="[Y/n]"
537
+ else
538
+ hint="[y/N]"
539
+ fi
540
+
541
+ result="$(read_choice_from_user "$prompt $hint ")"
542
+ result="${result,,}" # lowercase
543
+
544
+ if [[ -z "$result" ]]; then
545
+ if [[ "$default_yes" == "true" ]]; then
546
+ return 0
547
+ else
548
+ return 1
549
+ fi
550
+ fi
551
+
552
+ [[ "$result" == "y" || "$result" == "yes" ]]
553
+ }
554
+
555
+ # Prompt user to choose symlink or copy mode
556
+ prompt_link_mode() {
557
+ echo
558
+ echo "Symlink mode:"
559
+ echo " Pro: Skills auto-update when you 'git pull' in ~/.apollo-toolkit"
560
+ echo " Pro: No need to re-run installer after patch updates"
561
+ echo " Con: Changes pushed to the repo automatically reflect in your skills -"
562
+ echo " you may receive updates you did not intend to accept"
563
+ echo
564
+
565
+ if read_yes_no "Install skills as symlinks (recommended)?" "true"; then
566
+ LINK_MODE="symlink"
567
+ else
568
+ LINK_MODE="copy"
569
+ fi
570
+ echo "Using: $LINK_MODE"
571
+ }
572
+
573
+ # Prompt whether to include codex-exclusive skills in non-codex targets
574
+ prompt_include_exclusive() {
575
+ if [[ ${#CODEX_SKILL_PATHS[@]} -eq 0 ]]; then
576
+ return
577
+ fi
578
+
579
+ local has_non_codex=false
580
+ local mode
581
+ for mode in "${SELECTED_MODES[@]}"; do
582
+ if [[ "$mode" != "codex" ]]; then
583
+ has_non_codex=true
584
+ break
585
+ fi
586
+ done
587
+
588
+ if [[ "$has_non_codex" != "true" ]]; then
589
+ return
590
+ fi
591
+
592
+ local -a codex_only_names=()
593
+ local codex_path name
594
+ for codex_path in "${CODEX_SKILL_PATHS[@]}"; do
595
+ name="$(basename "$codex_path")"
596
+ codex_only_names+=("$name")
597
+ done
598
+
599
+ echo
600
+ echo "Exclusive skills detected:"
601
+ echo " The following skills are exclusive to codex: ${codex_only_names[*]}"
602
+ echo " Your selected non-codex targets: $(printf '%s\n' "${SELECTED_MODES[@]}" | grep -v codex | tr '\n' ' ')"
603
+
604
+ if read_yes_no "Install codex-exclusive skills to non-codex targets as well?" "false"; then
605
+ # Add codex skills to shared paths so they get installed everywhere
606
+ for codex_path in "${CODEX_SKILL_PATHS[@]}"; do
607
+ SHARED_SKILL_PATHS+=("$codex_path")
608
+ done
609
+ echo "Will include codex-exclusive skills in all targets."
610
+ fi
611
+ }
612
+
245
613
  choose_modes_interactive() {
246
614
  local choice raw_choice
247
615
  local -a choices
@@ -293,18 +661,64 @@ resolve_modes() {
293
661
  done
294
662
  }
295
663
 
664
+ # ---- Main ----
665
+
296
666
  main() {
297
- local mode
298
- SELECTED_MODES=()
667
+ local first_arg="${1:-}"
299
668
 
300
- if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
669
+ if [[ "$first_arg" == "-h" || "$first_arg" == "--help" ]]; then
301
670
  usage
302
671
  exit 0
303
672
  fi
304
673
 
674
+ # Parse --symlink / --copy flags
675
+ local -a filtered_args=()
676
+ for arg in "$@"; do
677
+ case "$arg" in
678
+ --symlink) LINK_MODE="symlink" ;;
679
+ --copy) LINK_MODE="copy" ;;
680
+ *) filtered_args+=("$arg") ;;
681
+ esac
682
+ done
683
+ set -- "${filtered_args[@]}"
684
+
685
+ first_arg="${1:-}"
686
+
687
+ # Uninstall path
688
+ if [[ "$first_arg" == "uninstall" ]]; then
689
+ shift
690
+ SELECTED_MODES=()
691
+ if [[ $# -gt 0 ]]; then
692
+ resolve_modes "$@"
693
+ fi
694
+ run_uninstall
695
+ exit 0
696
+ fi
697
+
698
+ # Install path (default). Skip "install" verb if present.
699
+ if [[ "$first_arg" == "install" ]]; then
700
+ shift
701
+ fi
702
+
703
+ SELECTED_MODES=()
305
704
  resolve_modes "$@"
306
705
  collect_skills
307
706
 
707
+ # Prompt for link mode if not set via CLI
708
+ if [[ -z "$LINK_MODE" ]]; then
709
+ prompt_link_mode
710
+ fi
711
+
712
+ # Prompt for exclusive skills inclusion
713
+ prompt_include_exclusive
714
+
715
+ # Show summary and confirm
716
+ echo
717
+ echo "Apollo Toolkit repo: $REPO_ROOT"
718
+ echo "Install mode: $LINK_MODE"
719
+ echo "Targets: ${SELECTED_MODES[*]}"
720
+ echo
721
+
308
722
  for mode in "${SELECTED_MODES[@]}"; do
309
723
  case "$mode" in
310
724
  codex) install_codex ;;