ai-global 1.4.4 → 1.5.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.
Files changed (2) hide show
  1. package/ai-global +910 -73
  2. package/package.json +1 -1
package/ai-global CHANGED
@@ -1,18 +1,10 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/bash
2
2
 
3
3
  # AI Global: Unified AI Tools Configuration Manager
4
4
  # https://github.com/nanxiaobei/ai-global
5
5
 
6
- VERSION="1.4.3"
6
+ set -e
7
7
 
8
- # Colors
9
- RED='\033[0;31m'
10
- GREEN='\033[0;32m'
11
- YELLOW='\033[1;33m'
12
- BLUE='\033[0;34m'
13
- NC='\033[0m'
14
-
15
- # Directories
16
8
  CONFIG_DIR="$HOME/.ai-global"
17
9
  BACKUP_DIR="$CONFIG_DIR/backups"
18
10
  GLOBAL_MD="$CONFIG_DIR/global.md"
@@ -22,13 +14,36 @@ RULES_DIR="$CONFIG_DIR/rules"
22
14
  COMMANDS_DIR="$CONFIG_DIR/commands"
23
15
  PROMPTS_DIR="$CONFIG_DIR/prompts"
24
16
 
25
- # Logging functions
26
- log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
27
- log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
28
- log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
29
- log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
17
+ # Version
18
+ VERSION=""
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ PACKAGE_JSON="$SCRIPT_DIR/package.json"
21
+
22
+ if [[ -f "$PACKAGE_JSON" ]]; then
23
+ VERSION=$(grep '"version"' "$PACKAGE_JSON" 2>/dev/null | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
24
+ fi
25
+
26
+ if [[ -z "$VERSION" ]]; then
27
+ VERSION="unknown"
28
+ fi
29
+
30
+ # Colors
31
+ RED='\033[0;31m'
32
+ GREEN='\033[0;32m'
33
+ YELLOW='\033[0;33m'
34
+ BLUE='\033[0;34m'
35
+ CYAN='\033[0;36m'
36
+ MAGENTA='\033[0;35m'
37
+ BRIGHT_RED='\033[1;31m'
38
+ BRIGHT_GREEN='\033[1;32m'
39
+ BRIGHT_YELLOW='\033[1;33m'
40
+ BRIGHT_BLUE='\033[1;34m'
41
+ BRIGHT_MAGENTA='\033[1;35m'
42
+ BRIGHT_CYAN='\033[1;36m'
43
+ NC='\033[0m'
30
44
 
31
- # Tool colors
45
+ # Tool color palette (using xterm-256 colors for more variety)
46
+ # We pick a spread of colors from the 256-color palette (avoiding too dark/grayscale)
32
47
  TOOL_COLORS=(
33
48
  "\033[38;5;39m" "\033[38;5;214m" "\033[38;5;118m" "\033[38;5;171m" "\033[38;5;208m"
34
49
  "\033[38;5;45m" "\033[38;5;190m" "\033[38;5;161m" "\033[38;5;111m" "\033[38;5;220m"
@@ -38,8 +53,20 @@ TOOL_COLORS=(
38
53
  "\033[38;5;121m" "\033[38;5;227m" "\033[38;5;165m" "\033[38;5;33m" "\033[38;5;216m"
39
54
  "\033[38;5;159m" "\033[38;5;178m" "\033[38;5;162m" "\033[38;5;117m" "\033[38;5;221m"
40
55
  "\033[38;5;78m" "\033[38;5;203m" "\033[38;5;113m" "\033[38;5;142m" "\033[38;5;211m"
56
+ "\033[38;5;159m" "\033[38;5;178m" "\033[38;5;162m" "\033[38;5;117m" "\033[38;5;221m"
57
+ "\033[38;5;78m" "\033[38;5;203m" "\033[38;5;113m" "\033[38;5;142m" "\033[38;5;211m"
41
58
  )
42
59
 
60
+ beautify_path() {
61
+ local path="$1"
62
+ if [[ "$path" == "$HOME"* ]]; then
63
+ local beautified="~${path#$HOME}"
64
+ echo "${beautified//\/\///}"
65
+ else
66
+ echo "$path"
67
+ fi
68
+ }
69
+
43
70
  get_tool_color() {
44
71
  local name="$1"
45
72
  local sum=0
@@ -50,17 +77,30 @@ get_tool_color() {
50
77
  echo -e "${TOOL_COLORS[$((sum % ${#TOOL_COLORS[@]}))]}"
51
78
  }
52
79
 
53
- beautify_path() {
54
- local path="$1"
55
- if [[ "$path" == "$HOME"* ]]; then
56
- local beautified="~${path#$HOME}"
57
- echo "${beautified//\/\///}"
80
+ # Extract name from frontmatter (name: "...")
81
+ extract_meta_name() {
82
+ local file="$1"
83
+ local default_name="$2"
84
+ if [[ ! -f "$file" ]]; then
85
+ echo "$default_name"
86
+ return
87
+ fi
88
+ # Match name: "value" or name: value
89
+ local extracted=$(grep -m 1 "^name:" "$file" | sed -E 's/^name:[[:space:]]*["'"'"'"'']?([^"'"'"'"'']+)["'"'"'"'']?/\1/' | xargs 2>/dev/null || true)
90
+ if [[ -n "$extracted" ]]; then
91
+ echo "$extracted"
58
92
  else
59
- echo "$path"
93
+ echo "$default_name"
60
94
  fi
61
95
  }
62
96
 
97
+ log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
98
+ log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
99
+ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
100
+ log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
101
+
63
102
  # Known AI tool patterns
103
+ # Format: dir|name|instr_file|skills_dir|agents_dir|rules_dir|commands_dir|prompts_dir
64
104
  declare -a KNOWN_PATTERNS=(
65
105
  ".claude|Claude Code|CLAUDE.md|skills|.|.|commands|."
66
106
  ".cursor|Cursor|rules/global.md|skills|.|.|.|prompts"
@@ -72,7 +112,7 @@ declare -a KNOWN_PATTERNS=(
72
112
  ".opencode|OpenCode|instructions.md|.|.|.|.|."
73
113
  ".qoder|Qoder|instructions.md|.|.|.|.|."
74
114
  ".qodo|Qodo|instructions.md|.|agents|.|.|."
75
- ".github|GitHub Copilot|copilot-instructions.md|.|.|.|.|."
115
+ ".config/github-copilot|GitHub Copilot|instructions.md|.|.|.|.|."
76
116
  ".aider|Aider|.aider.conf.yml|.|.|.|.|."
77
117
  ".continue|Continue|config.json|.|.|rules|.|prompts"
78
118
  ".codeium|Codeium|config.json|.|.|.|.|."
@@ -105,44 +145,809 @@ declare -a KNOWN_PATTERNS=(
105
145
  ".codebuddy|CodeBuddy|settings.json|.|agents|.|.|."
106
146
  )
107
147
 
108
- # Create directories
109
- mkdir -p "$CONFIG_DIR" "$BACKUP_DIR" "$SKILLS_DIR" "$AGENTS_DIR" "$RULES_DIR" "$COMMANDS_DIR" "$PROMPTS_DIR"
148
+ # Backup a file or directory
149
+ backup_item() {
150
+ local source="$1"
151
+ local tool_dir="$2"
152
+ local type="$3"
153
+
154
+ [[ ! -e "$source" ]] && return 0
155
+ [[ -L "$source" ]] && return 0
156
+
157
+ mkdir -p "$BACKUP_DIR"
158
+
159
+ local backup_name=$(echo "$tool_dir" | tr '/' '_')
160
+ local timestamp=$(date +%s)
161
+ local backup_path="$BACKUP_DIR/${backup_name}.${type}.${timestamp}"
162
+
163
+ if [[ -d "$source" ]]; then
164
+ cp -r "$source" "$backup_path" 2>/dev/null || return 0
165
+ else
166
+ cp "$source" "$backup_path" 2>/dev/null || return 0
167
+ fi
168
+
169
+ log_ok "Backed up: $source"
170
+ }
171
+
172
+ # Merge items from a tool to shared directory (dedup by filename)
173
+ merge_items() {
174
+ local source_dir="$1"
175
+ local target_dir="$2"
176
+ local type="$3"
177
+ local tool_name="$4"
178
+
179
+ [[ ! -d "$source_dir" ]] && return
180
+ [[ -L "$source_dir" ]] && return
181
+
182
+ local merged_count=0
183
+
184
+ for item in "$source_dir"/*; do
185
+ [[ ! -e "$item" ]] && continue
186
+ local name=$(basename "$item")
187
+ local target="$target_dir/$name"
188
+
189
+ [[ -e "$target" ]] && continue
190
+
191
+ if [[ -d "$item" ]]; then
192
+ cp -r "$item" "$target"
193
+ else
194
+ cp "$item" "$target"
195
+ fi
196
+ ((merged_count++))
197
+ done
198
+
199
+ if [[ $merged_count -gt 0 ]]; then
200
+ local tool_color=$(get_tool_color "$tool_name")
201
+ log_ok "Merged $merged_count $type from ${tool_color}${tool_name}${NC}"
202
+ fi
203
+ }
204
+
205
+ # Count items in directory (dirs and files)
206
+ count_items() {
207
+ local dir="$1"
208
+ if [[ -d "$dir" ]]; then
209
+ ls -1 "$dir" 2>/dev/null | wc -l | tr -d ' '
210
+ else
211
+ echo "0"
212
+ fi
213
+ }
214
+
215
+ # Create symlink
216
+ create_symlink() {
217
+ local source="$1"
218
+ local target="$2"
219
+
220
+ [[ ! -e "$source" ]] && return
110
221
 
111
- # Main function
222
+ local target_dir=$(dirname "$target")
223
+ mkdir -p "$target_dir"
224
+
225
+ # If target exists and is a real file/dir, it should have been backed up by backup_item already.
226
+ # We remove it to make room for the symlink, avoiding in-place backups.
227
+ if [[ -e "$target" ]] && [[ ! -L "$target" ]]; then
228
+ rm -rf "$target"
229
+ fi
230
+
231
+ [[ -L "$target" ]] && rm "$target"
232
+ ln -s "$source" "$target"
233
+ }
234
+
235
+ # Show symlink status
236
+ show_status() {
237
+ log_info "Symlink status:"
238
+ echo ""
239
+
240
+ local total_links=0
241
+
242
+ # Instructions
243
+ local instr_output=""
244
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
245
+ local p_dir p_name p_instr p_skills p_agents p_rules p_cmds p_prompts
246
+ IFS='|' read -r p_dir p_name p_instr p_skills p_agents p_rules p_cmds p_prompts <<< "$pattern"
247
+
248
+ if [[ "$p_instr" != "." ]] && [[ "$p_instr" != *.json ]] && [[ "$p_instr" != *.yml ]]; then
249
+ local target="$HOME/$p_dir/$p_instr"
250
+ if [[ -L "$target" ]]; then
251
+ local link_target=$(readlink "$target" 2>/dev/null || true)
252
+ if [[ "$link_target" == *".ai-global"* ]]; then
253
+ local tool_color=$(get_tool_color "$p_name")
254
+ instr_output+=" ${tool_color}$(beautify_path "$target")${NC}\n"
255
+ ((total_links++))
256
+ fi
257
+ fi
258
+ fi
259
+ done
260
+
261
+ if [[ -n "$instr_output" ]]; then
262
+ echo -e "${BLUE}[global.md]${NC}"
263
+ echo -e -n "$instr_output"
264
+ fi
265
+
266
+ for type_name in skills agents rules commands prompts; do
267
+ local type_output=""
268
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
269
+ local p_dir p_name p_instr p_skills p_agents p_rules p_cmds p_prompts
270
+ IFS='|' read -r p_dir p_name p_instr p_skills p_agents p_rules p_cmds p_prompts <<< "$pattern"
271
+
272
+ local type_dir=""
273
+ case "$type_name" in
274
+ skills) type_dir="$p_skills" ;;
275
+ agents) type_dir="$p_agents" ;;
276
+ rules) type_dir="$p_rules" ;;
277
+ commands) type_dir="$p_cmds" ;;
278
+ prompts) type_dir="$p_prompts" ;;
279
+ esac
280
+
281
+ if [[ "$type_dir" != "." ]]; then
282
+ local target="$HOME/$p_dir/$type_dir"
283
+ if [[ -L "$target" ]]; then
284
+ local link_target=$(readlink "$target" 2>/dev/null || true)
285
+ if [[ "$link_target" == *".ai-global"* ]]; then
286
+ local tool_color=$(get_tool_color "$p_name")
287
+ type_output+=" ${tool_color}$(beautify_path "$target")${NC}\n"
288
+ ((total_links++))
289
+ fi
290
+ fi
291
+ fi
292
+ done
293
+
294
+ if [[ -n "$type_output" ]]; then
295
+ echo -e "\n${BLUE}[$type_name]${NC}"
296
+ echo -e -n "$type_output"
297
+ fi
298
+ done
299
+
300
+ if [[ $total_links -eq 0 ]]; then
301
+ echo " No active symlinks found."
302
+ fi
303
+
304
+ echo ""
305
+ log_info "Shared items: skills=$(count_items "$SKILLS_DIR"), agents=$(count_items "$AGENTS_DIR"), rules=$(count_items "$RULES_DIR"), commands=$(count_items "$COMMANDS_DIR"), prompts=$(count_items "$PROMPTS_DIR")"
306
+ }
307
+
308
+ # List supported tools
309
+ list_supported() {
310
+ log_info "Supported AI tools:"
311
+ echo ""
312
+ printf " ${BLUE}%-20s %-22s %-10s %-10s %-10s %-10s %-10s %s${NC}\n" "Tool" "Directory" "Skills" "Agents" "Rules" "Cmds" "Prompts" "Status"
313
+ echo " --------------------------------------------------------------------------------------------------------------------------------"
314
+
315
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
316
+ local p_dir p_name p_instr p_skills p_agents p_rules p_cmds p_prompts
317
+ IFS='|' read -r p_dir p_name p_instr p_skills p_agents p_rules p_cmds p_prompts <<< "$pattern"
318
+ local full_path="$HOME/$p_dir"
319
+
320
+ local s_str="." a_str="." r_str="." c_str="." p_str="."
321
+ if [[ -d "$full_path" ]]; then
322
+ [[ "$p_skills" != "." && -d "$full_path/$p_skills" ]] && s_str="✓"
323
+ [[ "$p_agents" != "." && -d "$full_path/$p_agents" ]] && a_str="✓"
324
+ [[ "$p_rules" != "." && -d "$full_path/$p_rules" ]] && r_str="✓"
325
+ [[ "$p_cmds" != "." && -d "$full_path/$p_cmds" ]] && c_str="✓"
326
+ # Prompts can be a file or dir
327
+ [[ "$p_prompts" != "." && -e "$full_path/$p_prompts" ]] && p_str="✓"
328
+ fi
329
+
330
+ local status=""
331
+ local tool_color=""
332
+ if [[ -d "$full_path" ]]; then
333
+ status="${GREEN}Installed${NC}"
334
+ tool_color=$(get_tool_color "$p_name")
335
+ else
336
+ status="${YELLOW}Not found${NC}"
337
+ tool_color="${NC}"
338
+ fi
339
+
340
+ # Use manual padding because printf handles multibyte characters (✓) by byte count in Bash 3.2.
341
+ # Each category block matches the header's "%-10s " (11 characters total).
342
+ # We use indicator + 10 spaces = 11 characters.
343
+ printf " %b%-20s %-22s%b %s %s %s %s %s %b\n" \
344
+ "$tool_color" "$p_name" "$p_dir" "$NC" "$s_str" "$a_str" "$r_str" "$c_str" "$p_str" "$status"
345
+ done
346
+ echo ""
347
+ }
348
+
349
+ # List available backups
350
+ list_backups() {
351
+ log_info "Available backups:"
352
+ echo ""
353
+
354
+ # Use ls -A to catch hidden files/dirs (starting with .)
355
+ local backups_list=$(ls -A "$BACKUP_DIR" 2>/dev/null || true)
356
+
357
+ if [[ -z "$backups_list" ]]; then
358
+ echo " No backups found"
359
+ echo ""
360
+ return
361
+ fi
362
+
363
+ printf " ${BLUE}%-25s %-12s %s${NC}\n" "Tool" "Type" "Backup File"
364
+ echo " --------------------------------------------------------------------"
365
+
366
+ while read -r filename; do
367
+ [[ -z "$filename" ]] && continue
368
+ local tool_name=""
369
+ local type=""
370
+
371
+ # Improved regex to handle various path characters
372
+ if [[ "$filename" =~ ^(.+)\.([^\.]+)\.([0-9]+)$ ]]; then
373
+ tool_name="${BASH_REMATCH[1]}"
374
+ type="${BASH_REMATCH[2]}"
375
+ else
376
+ tool_name="$filename"
377
+ type="unknown"
378
+ fi
379
+
380
+ local tool_color=$(get_tool_color "${tool_name//_/ }")
381
+ # Print the backup filename as a path prefixed with ~/ using beautify_path
382
+ local backup_path=$(beautify_path "$BACKUP_DIR/$filename")
383
+ printf " %s%-25s %-12s %s%b\n" "$tool_color" "$tool_name" "$type" "$backup_path" "$NC"
384
+ done <<< "$backups_list"
385
+ echo ""
386
+ }
387
+
388
+ # Collect and merge instructions from all tools
389
+ collect_instructions() {
390
+ local merged_content=""
391
+ local found_count=0
392
+
393
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
394
+ IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
395
+
396
+ if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
397
+ local actual_path="$HOME/$dir_name/$instr_file"
398
+ [[ -L "$actual_path" ]] && continue
399
+
400
+ if [[ -f "$actual_path" ]]; then
401
+ local content=$(cat "$actual_path" 2>/dev/null)
402
+ if [[ -n "$content" ]]; then
403
+ if [[ $found_count -gt 0 ]]; then
404
+ merged_content+="\n\n---\n\n"
405
+ fi
406
+ merged_content+="# From $tool_name\n\n$content"
407
+ ((found_count++))
408
+ fi
409
+ fi
410
+ fi
411
+ done
412
+
413
+ if [[ $found_count -gt 0 ]]; then
414
+ echo -e "$merged_content" > "$GLOBAL_MD"
415
+ log_ok "Merged instructions from $found_count tool(s)"
416
+ else
417
+ cat > "$GLOBAL_MD" << 'EOF'
418
+ # AI Assistant Instructions
419
+
420
+ <!-- Add your instructions here. They will sync to all AI tools. -->
421
+ EOF
422
+ log_ok "Created: $GLOBAL_MD"
423
+ fi
424
+ }
425
+
426
+ # Update: scan, merge and link tools
112
427
  update_tools() {
113
428
  log_info "Scanning for AI tools..."
114
429
  echo ""
115
-
430
+
431
+ mkdir -p "$SKILLS_DIR" "$AGENTS_DIR" "$RULES_DIR" "$COMMANDS_DIR" "$PROMPTS_DIR" "$BACKUP_DIR"
432
+
433
+ collect_instructions
434
+
116
435
  local tool_count=0
117
-
436
+
118
437
  for pattern in "${KNOWN_PATTERNS[@]}"; do
119
438
  IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
120
439
  local full_path="$HOME/$dir_name"
121
-
440
+
122
441
  if [[ -d "$full_path" ]]; then
123
- local tool_color=$(get_tool_color "$tool_name")
124
- echo -e " ${tool_color}${tool_name}${NC} found in $(beautify_path "$full_path")"
442
+ local color=$(get_tool_color "$tool_name")
443
+ echo -e "${GREEN}[OK]${NC} ${color}Found: $tool_name${NC}"
125
444
  ((tool_count++))
445
+
446
+ if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
447
+ local actual_path="$HOME/$dir_name/$instr_file"
448
+ backup_item "$actual_path" "$dir_name" "instructions"
449
+ fi
450
+
451
+ for type_name in skills agents rules commands prompts; do
452
+ local type_dir=""
453
+ case "$type_name" in
454
+ skills) type_dir="$skills" ;;
455
+ agents) type_dir="$agents" ;;
456
+ rules) type_dir="$rules" ;;
457
+ commands) type_dir="$commands" ;;
458
+ prompts) type_dir="$prompts" ;;
459
+ esac
460
+ if [[ "$type_dir" != "." ]]; then
461
+ local path="$HOME/$dir_name/$type_dir"
462
+ backup_item "$path" "$dir_name" "$type_name"
463
+ local target_dir=""
464
+ case "$type_name" in
465
+ skills) target_dir="$SKILLS_DIR" ;;
466
+ agents) target_dir="$AGENTS_DIR" ;;
467
+ rules) target_dir="$RULES_DIR" ;;
468
+ commands) target_dir="$COMMANDS_DIR" ;;
469
+ prompts) target_dir="$PROMPTS_DIR" ;;
470
+ esac
471
+ merge_items "$path" "$target_dir" "$type_name" "$tool_name"
472
+ fi
473
+ done
474
+ fi
475
+ done
476
+
477
+ if [[ $tool_count -eq 0 ]]; then
478
+ log_info "No AI tools found."
479
+ return
480
+ fi
481
+
482
+ echo ""
483
+ log_info "Creating symlinks..."
484
+
485
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
486
+ IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
487
+ local full_path="$HOME/$dir_name"
488
+
489
+ if [[ -d "$full_path" ]]; then
490
+ local tool_color=$(get_tool_color "$tool_name")
491
+ if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
492
+ local target="$HOME/$dir_name/$instr_file"
493
+ create_symlink "$GLOBAL_MD" "$target"
494
+ printf " ${tool_color}✓ %-40s -> %s${NC}\n" "$(beautify_path "$target")" "$(beautify_path "$GLOBAL_MD")"
495
+ fi
496
+
497
+ for type_name in skills agents rules commands prompts; do
498
+ local type_dir=""
499
+ local source_dir=""
500
+ case "$type_name" in
501
+ skills) type_dir="$skills"; source_dir="$SKILLS_DIR" ;;
502
+ agents) type_dir="$agents"; source_dir="$AGENTS_DIR" ;;
503
+ rules) type_dir="$rules"; source_dir="$RULES_DIR" ;;
504
+ commands) type_dir="$commands"; source_dir="$COMMANDS_DIR" ;;
505
+ prompts) type_dir="$prompts"; source_dir="$PROMPTS_DIR" ;;
506
+ esac
507
+ if [[ "$type_dir" != "." ]]; then
508
+ local target="$HOME/$dir_name/$type_dir"
509
+ create_symlink "$source_dir" "$target"
510
+ printf " ${tool_color}✓ %-40s -> %s${NC}\n" "$(beautify_path "$target")" "$(beautify_path "$source_dir")/"
511
+ fi
512
+ done
126
513
  fi
127
514
  done
128
-
515
+
129
516
  echo ""
130
- log_info "Found $tool_count AI tools"
131
- log_info "Shared directories: skills=$(count_items "$SKILLS_DIR"), agents=$(count_items "$AGENTS_DIR"), rules=$(count_items "$RULES_DIR"), commands=$(count_items "$COMMANDS_DIR"), prompts=$(count_items "$PROMPTS_DIR")"
517
+ log_info "Done! Shared: skills=$(count_items "$SKILLS_DIR"), agents=$(count_items "$AGENTS_DIR"), rules=$(count_items "$RULES_DIR"), commands=$(count_items "$COMMANDS_DIR"), prompts=$(count_items "$PROMPTS_DIR")"
132
518
  }
133
519
 
134
- count_items() {
135
- local dir="$1"
136
- if [[ -d "$dir" ]]; then
137
- find "$dir" -type f 2>/dev/null | wc -l
520
+ # Unlink a single tool
521
+ unlink_single_tool() {
522
+ local tool_name="$1"
523
+ local dir_name="$2"
524
+ local instr_file="$3"
525
+ local skills="$4"
526
+ local agents="$5"
527
+ local rules="$6"
528
+ local commands="$7"
529
+ local prompts="$8"
530
+ local silent="${9:-false}"
531
+
532
+ local backup_name=$(echo "$dir_name" | tr '/' '_')
533
+ local worked=false
534
+
535
+ # Check for instructions link
536
+ if [[ "$instr_file" != "." ]]; then
537
+ local target="$HOME/$dir_name/$instr_file"
538
+ if [[ -L "$target" ]]; then
539
+ local link_target=$(readlink "$target" 2>/dev/null || true)
540
+ if [[ "$link_target" == *".ai-global"* ]]; then
541
+ rm "$target"
542
+ local backup_file=$(ls -t "$BACKUP_DIR"/${backup_name}.instructions.* 2>/dev/null | head -1)
543
+ [[ -f "$backup_file" ]] && cp "$backup_file" "$target"
544
+ worked=true
545
+ fi
546
+ fi
547
+ fi
548
+
549
+ # Check for components links
550
+ for type_name in skills agents rules commands prompts; do
551
+ local type_dir=""
552
+ case "$type_name" in
553
+ skills) type_dir="$skills" ;;
554
+ agents) type_dir="$agents" ;;
555
+ rules) type_dir="$rules" ;;
556
+ commands) type_dir="$commands" ;;
557
+ prompts) type_dir="$prompts" ;;
558
+ esac
559
+ if [[ "$type_dir" != "." ]]; then
560
+ local target="$HOME/$dir_name/$type_dir"
561
+ if [[ -L "$target" ]]; then
562
+ local link_target=$(readlink "$target" 2>/dev/null || true)
563
+ if [[ "$link_target" == *".ai-global"* ]]; then
564
+ rm "$target"
565
+ local backup_file=$(ls -td "$BACKUP_DIR"/${backup_name}.${type_name}.* 2>/dev/null | head -1)
566
+ if [[ -d "$backup_file" ]]; then
567
+ cp -r "$backup_file" "$target"
568
+ fi
569
+ worked=true
570
+ fi
571
+ fi
572
+ fi
573
+ done
574
+
575
+ # Check for backups persistence
576
+ local has_backups=false
577
+ if [[ -n "$(ls "$BACKUP_DIR"/${backup_name}.* 2>/dev/null)" ]]; then
578
+ has_backups=true
579
+ rm -rf "$BACKUP_DIR"/${backup_name}.* 2>/dev/null || true
580
+ fi
581
+
582
+ if [[ "$worked" == true ]] || [[ "$has_backups" == true ]]; then
583
+ if [[ "$silent" != "true" ]]; then
584
+ local color=$(get_tool_color "$tool_name")
585
+ echo -e "${GREEN}[OK]${NC} ${color}Unlinked: $tool_name${NC}"
586
+ fi
587
+ return 0
588
+ fi
589
+
590
+ return 1
591
+ }
592
+
593
+ # Unlink all tools
594
+ unlink_all_tools() {
595
+ log_info "Unlinking tools..."
596
+ echo ""
597
+
598
+ local unlinked_count=0
599
+ # Scan all known patterns to find and remove any symlinks
600
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
601
+ IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
602
+ if unlink_single_tool "$tool_name" "$dir_name" "$instr_file" "$skills" "$agents" "$rules" "$commands" "$prompts"; then
603
+ ((unlinked_count++))
604
+ fi
605
+ done
606
+
607
+ # Final sweep for any remaining symlinks pointing to .ai-global
608
+ find "$HOME" -maxdepth 3 -type l 2>/dev/null | while read -r link; do
609
+ local target=$(readlink "$link" 2>/dev/null || true)
610
+ if [[ "$target" == *".ai-global"* ]]; then
611
+ rm "$link"
612
+ log_ok "Removed unknown symlink: $(beautify_path "$link")"
613
+ ((unlinked_count++))
614
+ fi
615
+ done
616
+
617
+ # Clear all backups as requested
618
+ rm -rf "$BACKUP_DIR"/* 2>/dev/null || true
619
+
620
+ echo ""
621
+ if [[ $unlinked_count -gt 0 ]]; then
622
+ log_info "Unlinked $unlinked_count item(s) and cleared backups. Shared data preserved."
138
623
  else
139
- echo "0"
624
+ log_info "No active symlinks found. Backups cleared."
140
625
  fi
141
626
  }
142
627
 
628
+ # Unlink a specific tool
629
+ unlink_tool() {
630
+ local tool_query="$1"
631
+
632
+ if [[ -z "$tool_query" ]]; then
633
+ log_error "Usage: ai-global unlink <tool> or ai-global unlink all"
634
+ echo ""
635
+ list_backups
636
+ return 1
637
+ fi
638
+
639
+ if [[ "$tool_query" == "all" ]]; then
640
+ unlink_all_tools
641
+ return
642
+ fi
643
+
644
+ local found=false
645
+
646
+ for pattern in "${KNOWN_PATTERNS[@]}"; do
647
+ IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
648
+ local tool_lower=$(echo "$tool_name" | tr '[:upper:]' '[:lower:]')
649
+ local query_lower=$(echo "$tool_query" | tr '[:upper:]' '[:lower:]')
650
+
651
+ if [[ "$tool_lower" == *"$query_lower"* ]] || [[ "$dir_name" == *"$query_lower"* ]]; then
652
+ if ! unlink_single_tool "$tool_name" "$dir_name" "$instr_file" "$skills" "$agents" "$rules" "$commands" "$prompts"; then
653
+ log_info "$tool_name is not currently linked."
654
+ fi
655
+ found=true
656
+ break
657
+ fi
658
+ done
659
+
660
+ if [[ "$found" == false ]]; then
661
+ log_error "Tool not found: $tool_query"
662
+ echo ""
663
+ echo "Use 'ai-global list' to see supported tools"
664
+ return 1
665
+ fi
666
+ }
667
+
668
+ # Check if input is a GitHub reference
669
+ is_github_ref() {
670
+ local input="$1"
671
+ # Match: user/repo, https://github.com/user/repo, github.com/user/repo
672
+ if [[ "$input" =~ ^https?://github\.com/ ]] || \
673
+ [[ "$input" =~ ^github\.com/ ]] || \
674
+ [[ "$input" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+(/.*)?$ ]]; then
675
+ return 0
676
+ fi
677
+ return 1
678
+ }
679
+
680
+ # Parse GitHub reference to get owner, repo, and optional path
681
+ parse_github_ref() {
682
+ local input="$1"
683
+
684
+ # Remove https://github.com/ or github.com/ prefix
685
+ input="${input#https://github.com/}"
686
+ input="${input#http://github.com/}"
687
+ input="${input#github.com/}"
688
+
689
+ # Remove /blob/main/ or /blob/master/ or /tree/main/ etc for file/dir paths
690
+ input=$(echo "$input" | sed -E 's|/blob/[^/]+/|/|; s|/tree/[^/]+/|/|')
691
+
692
+ echo "$input"
693
+ }
694
+
695
+ # Download from GitHub
696
+ download_from_github() {
697
+ local type="$1"
698
+ local ref="$2"
699
+ local target_dir="$3"
700
+
701
+ local parsed=$(parse_github_ref "$ref")
702
+ local owner=$(echo "$parsed" | cut -d'/' -f1)
703
+ local repo=$(echo "$parsed" | cut -d'/' -f2)
704
+ local path=$(echo "$parsed" | cut -d'/' -f3-)
705
+
706
+ if [[ -z "$owner" ]] || [[ -z "$repo" ]]; then
707
+ log_error "Invalid GitHub reference: $ref"
708
+ return 1
709
+ fi
710
+
711
+ # If path points to a specific file
712
+ if [[ -n "$path" ]] && [[ "$path" == *.md ]]; then
713
+ local filename=$(basename "$path")
714
+ local raw_url="https://raw.githubusercontent.com/$owner/$repo/main/$path"
715
+
716
+ log_info "Downloading: $raw_url"
717
+
718
+ if curl -fsSL "$raw_url" -o "$target_dir/$filename" 2>/dev/null; then
719
+ log_ok "Added $type: $target_dir/$filename"
720
+ else
721
+ # Try master branch
722
+ raw_url="https://raw.githubusercontent.com/$owner/$repo/master/$path"
723
+ if curl -fsSL "$raw_url" -o "$target_dir/$filename" 2>/dev/null; then
724
+ log_ok "Added $type: $target_dir/$filename"
725
+ else
726
+ log_error "Failed to download: $ref"
727
+ return 1
728
+ fi
729
+ fi
730
+ else
731
+ # Clone entire repo or subdirectory
732
+ local tmp_dir=$(mktemp -d)
733
+ local clone_url="https://github.com/$owner/$repo.git"
734
+
735
+ log_info "Cloning: $clone_url"
736
+
737
+ if git clone --depth 1 --single-branch "$clone_url" "$tmp_dir/$repo"; then
738
+ local source_dir="$tmp_dir/$repo"
739
+ [[ -n "$path" ]] && source_dir="$tmp_dir/$repo/$path"
740
+
741
+ if [[ -d "$source_dir" ]]; then
742
+ local count=0
743
+ local meta_file=""
744
+ case "$type" in
745
+ skill) meta_file="SKILL.md" ;;
746
+ agent) meta_file="AGENT.md" ;;
747
+ esac
748
+
749
+ # 1. Check if root contains metadata file
750
+ if [[ -n "$meta_file" ]] && [[ -f "$source_dir/$meta_file" ]]; then
751
+ local name=$(extract_meta_name "$source_dir/$meta_file" "$(basename "$source_dir")")
752
+ local target_path="$target_dir/$name"
753
+ mkdir -p "$target_path"
754
+ cp -R "$source_dir"/* "$target_path/"
755
+ count=1
756
+ fi
757
+
758
+ # 2. Check type subdirectories (skills/, agents/)
759
+ if [[ $count -eq 0 ]] && [[ -z "$path" ]]; then
760
+ local search_dirs=""
761
+ case "$type" in
762
+ skill) search_dirs="skills skill" ;;
763
+ agent) search_dirs="agents agent" ;;
764
+ rule) search_dirs="rules rule" ;;
765
+ esac
766
+
767
+ for dir in $search_dirs; do
768
+ if [[ -d "$source_dir/$dir" ]]; then
769
+ # Look for subdirectories containing SKILL.md/AGENT.md
770
+ if [[ -n "$meta_file" ]]; then
771
+ for d in "$source_dir/$dir"/*; do
772
+ [[ ! -d "$d" ]] && continue
773
+ if [[ -f "$d/$meta_file" ]]; then
774
+ local name=$(extract_meta_name "$d/$meta_file" "$(basename "$d")")
775
+ mkdir -p "$target_dir/$name"
776
+ cp -R "$d"/* "$target_dir/$name/"
777
+ ((count++))
778
+ fi
779
+ done
780
+ fi
781
+
782
+ # If we found items or if it's "rules" (no metadata file needed usually), we are done with this dir
783
+ if [[ $count -gt 0 ]] || [[ "$type" == "rule" ]]; then
784
+ source_dir="$source_dir/$dir"
785
+ break
786
+ fi
787
+ fi
788
+ done
789
+ fi
790
+
791
+ # 3. Check src/ directory if still nothing (for skills)
792
+ if [[ $count -eq 0 ]] && [[ "$type" == "skill" ]] && [[ -d "$source_dir/src" ]]; then
793
+ if [[ -f "$source_dir/src/$meta_file" ]]; then
794
+ local name=$(extract_meta_name "$source_dir/src/$meta_file" "$(basename "$source_dir")")
795
+ mkdir -p "$target_dir/$name"
796
+ cp -R "$source_dir/src"/* "$target_dir/$name/"
797
+ count=1
798
+ fi
799
+ fi
800
+
801
+ # Fallback check (rules only): copy individual .md files
802
+ if [[ $count -eq 0 ]] && [[ "$type" == "rule" ]]; then
803
+ for file in "$source_dir"/*.md; do
804
+ [[ ! -f "$file" ]] && continue
805
+ local filename=$(basename "$file")
806
+ if [[ "$filename" == "README.md" ]]; then
807
+ local other_mds=$(ls "$source_dir"/*.md 2>/dev/null | grep -v "README.md" | wc -l)
808
+ [[ $other_mds -gt 0 ]] && continue
809
+ fi
810
+ cp "$file" "$target_dir/$filename"
811
+ ((count++))
812
+ done
813
+ fi
814
+
815
+ if [[ $count -gt 0 ]]; then
816
+ log_ok "Added $count ${type}(s) from $owner/$repo"
817
+ else
818
+ # Show actual searched path
819
+ local searched_path="${source_dir#$tmp_dir/$repo}"
820
+ searched_path="${searched_path#/}"
821
+ log_warn "No $type found organized in $owner/$repo${searched_path:+/$searched_path}"
822
+ fi
823
+ else
824
+ log_error "Path not found: $path"
825
+ rm -rf "$tmp_dir"
826
+ return 1
827
+ fi
828
+
829
+ rm -rf "$tmp_dir"
830
+ else
831
+ rm -rf "$tmp_dir"
832
+ log_error "Failed to clone: $clone_url"
833
+ return 1
834
+ fi
835
+ fi
836
+ }
837
+
838
+ # Add item to a type directory
839
+ add_item() {
840
+ local type="$1"
841
+ local input="$2"
842
+
843
+ if [[ -z "$input" ]]; then
844
+ log_error "Usage: ai-global $type <file|github-repo>"
845
+ echo ""
846
+ echo "Examples:"
847
+ echo " ai-global $type react.md"
848
+ echo " ai-global $type /path/to/file.md"
849
+ echo " ai-global $type user/repo"
850
+ echo " ai-global $type https://github.com/user/repo"
851
+ echo " ai-global $type user/repo/path/to/file.md"
852
+ return 1
853
+ fi
854
+
855
+ local target_dir=""
856
+ case "$type" in
857
+ skill) target_dir="$SKILLS_DIR" ;;
858
+ agent) target_dir="$AGENTS_DIR" ;;
859
+ rule) target_dir="$RULES_DIR" ;;
860
+ command) target_dir="$COMMANDS_DIR" ;;
861
+ prompt) target_dir="$PROMPTS_DIR" ;;
862
+ esac
863
+
864
+ mkdir -p "$target_dir"
865
+
866
+ # Check if it's a GitHub reference
867
+ if is_github_ref "$input"; then
868
+ download_from_github "$type" "$input" "$target_dir"
869
+ elif [[ -f "$input" ]]; then
870
+ # Local file
871
+ local basename=$(basename "$input")
872
+ cp "$input" "$target_dir/$basename"
873
+ log_ok "Added $type: $target_dir/$basename"
874
+ else
875
+ # Create new file
876
+ local target_file="$target_dir/$input"
877
+ if [[ ! "$input" == *.md ]]; then
878
+ target_file="$target_dir/${input}.md"
879
+ fi
880
+ touch "$target_file"
881
+ log_ok "Created $type: $target_file"
882
+ echo "Edit: $target_file"
883
+ fi
884
+ }
885
+
886
+ # Uninstall
887
+ uninstall() {
888
+ log_warn "This will:"
889
+ echo " 1. Unlink all tools to original configuration"
890
+ echo " 2. Remove ~/.ai-global directory"
891
+ echo " 3. Remove ai-global from PATH"
892
+ echo ""
893
+ read -p "Are you sure? (y/N) " -r
894
+ echo ""
895
+
896
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
897
+ log_info "Cancelled"
898
+ return
899
+ fi
900
+
901
+ unlink_all_tools
902
+
903
+ [[ -L /usr/local/bin/ai-global ]] && rm -f /usr/local/bin/ai-global
904
+ [[ -L "$HOME/.local/bin/ai-global" ]] && rm -f "$HOME/.local/bin/ai-global"
905
+
906
+ rm -rf "$CONFIG_DIR"
907
+
908
+ log_ok "AI Global uninstalled"
909
+ }
910
+
143
911
  # Show version
144
912
  show_version() {
145
- echo "AI Global v$VERSION"
913
+ echo "ai-global version $VERSION"
914
+ }
915
+
916
+ # Upgrade
917
+ upgrade() {
918
+ log_info "Checking for updates..."
919
+
920
+ local remote_version
921
+ remote_version=$(curl -fsSL "https://raw.githubusercontent.com/nanxiaobei/ai-global/main/package.json" 2>/dev/null | grep '"version"' | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
922
+
923
+ if [[ -z "$remote_version" ]]; then
924
+ log_warn "Could not check for updates"
925
+ return 1
926
+ fi
927
+
928
+ if [[ "$remote_version" == "$VERSION" ]]; then
929
+ log_ok "Already at latest version ($VERSION)"
930
+ return 0
931
+ fi
932
+
933
+ log_info "Upgrading: $VERSION -> $remote_version"
934
+
935
+ local current_script="$0"
936
+ # If running via symlink, update the target
937
+ if [[ -L "$current_script" ]]; then
938
+ current_script=$(readlink "$current_script")
939
+ fi
940
+
941
+ local tmp_file=$(mktemp)
942
+ if curl -fsSL "https://raw.githubusercontent.com/nanxiaobei/ai-global/main/ai-global" -o "$tmp_file" 2>/dev/null; then
943
+ chmod +x "$tmp_file"
944
+ mv "$tmp_file" "$current_script"
945
+ log_ok "Upgraded to v$remote_version"
946
+ else
947
+ rm -f "$tmp_file"
948
+ log_error "Failed to download update"
949
+ return 1
950
+ fi
146
951
  }
147
952
 
148
953
  # Show help
@@ -150,40 +955,72 @@ show_help() {
150
955
  echo -e "${BLUE}AI Global: Unified AI Tools Configuration Manager${NC} v$VERSION"
151
956
  echo ""
152
957
  echo -e "${BLUE}USAGE:${NC}"
153
- echo " ai-global [command]"
958
+ echo -e " ai-global [command]"
959
+ echo ""
960
+ echo -e "${BLUE}CORE COMMANDS:${NC}"
961
+ echo -e " ${GREEN}(default)${NC} Scan, merge, update symlinks"
962
+ echo -e " ${GREEN}status${NC} Show symlink status"
963
+ echo -e " ${GREEN}list${NC} List all supported AI tools"
964
+ echo -e " ${GREEN}backups${NC} List available backups"
965
+ echo -e " ${GREEN}unlink <key>${NC} Restore a tool's original config"
966
+ echo -e " ${GREEN}unlink all${NC} Restore all tools"
967
+ echo ""
968
+ echo -e "${BLUE}RESOURCE MANAGEMENT:${NC}"
969
+ echo -e " ${GREEN}skill <user/repo>${NC} Add a skill"
970
+ echo -e " ${GREEN}agent <source>${NC} Add an agent"
971
+ echo -e " ${GREEN}rule <source>${NC} Add a rule"
972
+ echo -e " ${GREEN}command <source>${NC} Add a command"
973
+ echo -e " ${GREEN}prompt <source>${NC} Add a prompt"
974
+ echo ""
975
+ echo -e "${BLUE}SYSTEM COMMANDS:${NC}"
976
+ echo -e " ${GREEN}upgrade${NC} Upgrade ai-global to latest version"
977
+ echo -e " ${GREEN}uninstall${NC} Completely remove ai-global"
978
+ echo -e " ${GREEN}version${NC} Show version"
979
+ echo -e " ${GREEN}help${NC} Show this help"
154
980
  echo ""
155
- echo -e "${BLUE}COMMANDS:${NC}"
156
- echo " [default] Scan and show tool status"
157
- echo " status Show symlink status"
158
- echo " list List all supported AI tools"
159
- echo " version Show version"
160
- echo " help Show this help"
161
981
  }
162
982
 
163
- # Main command handler
164
- case "${1:-}" in
165
- "version"|"-v"|"--version")
983
+ # Main
984
+ main() {
985
+ local cmd="${1:-update}"
986
+
987
+ if [[ "$1" == "-v" ]] || [[ "$1" == "--version" ]] || [[ "$1" == "version" ]]; then
166
988
  show_version
167
- ;;
168
- "help"|"-h"|"--help")
169
- show_help
170
- ;;
171
- "status")
172
- update_tools
173
- ;;
174
- "list")
175
- echo "Supported AI Tools:"
176
- for pattern in "${KNOWN_PATTERNS[@]}"; do
177
- IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
178
- echo " - $tool_name"
179
- done
180
- ;;
181
- ""|"update")
182
- update_tools
183
- ;;
184
- *)
185
- echo "Unknown command: $1"
186
- echo "Use 'ai-global help' for usage"
187
- exit 1
188
- ;;
189
- esac
989
+ exit 0
990
+ fi
991
+
992
+ case "$cmd" in
993
+ help|--help|-h) show_help; exit 0 ;;
994
+ list) list_supported; exit 0 ;;
995
+ version|-v|--version) show_version; exit 0 ;;
996
+ skill|agent|rule|command|prompt|unlink|status|backups|upgrade|uninstall)
997
+ if [[ ! -d "$CONFIG_DIR" ]]; then
998
+ log_info "No configuration found. Running initial scan..."
999
+ update_tools
1000
+ [[ "$cmd" == "skill" || "$cmd" == "agent" || "$cmd" == "rule" || "$cmd" == "command" || "$cmd" == "prompt" || "$cmd" == "status" ]] || exit 0
1001
+ fi
1002
+ ;;
1003
+ version|-v|--version|help|--help|-h) ;;
1004
+ *) cmd="update" ;;
1005
+ esac
1006
+
1007
+ case "$cmd" in
1008
+ update) update_tools ;;
1009
+ status) show_status ;;
1010
+ list) list_supported ;;
1011
+ backups) list_backups ;;
1012
+ unlink) unlink_tool "$2" ;;
1013
+ skill) add_item "skill" "$2" ;;
1014
+ agent) add_item "agent" "$2" ;;
1015
+ rule) add_item "rule" "$2" ;;
1016
+ command) add_item "command" "$2" ;;
1017
+ prompt) add_item "prompt" "$2" ;;
1018
+ upgrade) upgrade ;;
1019
+ uninstall) uninstall ;;
1020
+ version|-v|--version) show_version ;;
1021
+ help|--help|-h) show_help ;;
1022
+ *) log_error "Unknown command: $cmd"; show_help; exit 1 ;;
1023
+ esac
1024
+ }
1025
+
1026
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-global",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "description": "Unified configuration manager for AI coding assistants",
5
5
  "bin": {
6
6
  "ai-global": "ai-global"