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.
- package/ai-global +910 -73
- package/package.json +1 -1
package/ai-global
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
#!/
|
|
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
|
-
|
|
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
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 "$
|
|
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|
|
|
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
|
-
#
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
echo -e "
|
|
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 "
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
164
|
-
|
|
165
|
-
"
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
;;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
;;
|
|
189
|
-
|
|
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 "$@"
|