ai-global 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/ai-global +919 -0
- package/package.json +39 -0
- package/scripts/postinstall.js +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# AI Config
|
|
2
|
+
|
|
3
|
+
[中文文档](README_CN.md)
|
|
4
|
+
|
|
5
|
+
Unified configuration manager for AI coding assistants. Edit one file, sync to all your AI tools.
|
|
6
|
+
|
|
7
|
+
## Supported Tools
|
|
8
|
+
|
|
9
|
+
| Tool | Instructions | Skills | Agents | Rules | Commands | Prompts |
|
|
10
|
+
|------|:------:|:------:|:------:|:-----:|:--------:|:-------:|
|
|
11
|
+
| Claude Code | ✓ | ✓ | | | ✓ | |
|
|
12
|
+
| Cursor | ✓ | ✓ | | | | ✓ |
|
|
13
|
+
| GitHub Copilot | ✓ | | | | | |
|
|
14
|
+
| Factory Droid | ✓ | | | | | |
|
|
15
|
+
| Gemini CLI | ✓ | ✓ | | | ✓ | |
|
|
16
|
+
| Windsurf | ✓ | ✓ | ✓ | ✓ | | |
|
|
17
|
+
| Kiro | ✓ | | ✓ | | | |
|
|
18
|
+
| Qodo | ✓ | | ✓ | | | |
|
|
19
|
+
| Antigravity | ✓ | ✓ | | | ✓ | |
|
|
20
|
+
| Continue | ✓ | | | ✓ | | ✓ |
|
|
21
|
+
| Cline | ✓ | | | ✓ | | ✓ |
|
|
22
|
+
| Roo Code | ✓ | | | ✓ | | |
|
|
23
|
+
| Sourcegraph Cody | ✓ | | | | ✓ | |
|
|
24
|
+
| CodeGPT | ✓ | | | | | ✓ |
|
|
25
|
+
| GPT Engineer | ✓ | | | | | ✓ |
|
|
26
|
+
| Smol Developer | ✓ | | | | | ✓ |
|
|
27
|
+
| Amp | ✓ | | | | | |
|
|
28
|
+
| Trae | ✓ | | | | | |
|
|
29
|
+
| OpenCode | ✓ | | | | | |
|
|
30
|
+
| OpenAI Codex | ✓ | | | | | |
|
|
31
|
+
| Aider | ✓ | | | | | |
|
|
32
|
+
| Codeium | ✓ | | | | | |
|
|
33
|
+
| TabNine | ✓ | | | | | |
|
|
34
|
+
| Zed | ✓ | | | | | |
|
|
35
|
+
| Aide | ✓ | | | | | |
|
|
36
|
+
| PearAI | ✓ | | | | | |
|
|
37
|
+
| Supermaven | ✓ | | | | | |
|
|
38
|
+
| CodeStory | ✓ | | | | | |
|
|
39
|
+
| Double | ✓ | | | | | |
|
|
40
|
+
| Blackbox AI | ✓ | | | | | |
|
|
41
|
+
| Amazon Q | ✓ | | | | | |
|
|
42
|
+
| Copilot Workspace | ✓ | | | | | |
|
|
43
|
+
| Goose AI | ✓ | | | | | |
|
|
44
|
+
| Mentat | ✓ | | | | | |
|
|
45
|
+
| Melty | ✓ | | | | | |
|
|
46
|
+
| Void | ✓ | | | | | |
|
|
47
|
+
| Qoder | ✓ | | | | | |
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
### npm / pnpm / yarn / bun
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g ai-global
|
|
55
|
+
# or
|
|
56
|
+
pnpm add -g ai-global
|
|
57
|
+
# or
|
|
58
|
+
yarn global add ai-global
|
|
59
|
+
# or
|
|
60
|
+
bun add -g ai-global
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### curl
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
curl -fsSL https://raw.githubusercontent.com/nanxiaobei/ai-global/main/install.sh | bash
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### First run
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ai-global
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This will:
|
|
78
|
+
1. Scan your system for installed AI tools
|
|
79
|
+
2. Backup original configs to `~/.ai-global/backups/`
|
|
80
|
+
3. Merge instructions/skills/agents/rules/commands/prompts from all tools
|
|
81
|
+
4. Create symlinks from each tool's config to shared directories
|
|
82
|
+
|
|
83
|
+
### Edit instructions
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
~/.ai-global/instructions.md
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Changes take effect immediately - all tools read the same file via symlinks.
|
|
90
|
+
|
|
91
|
+
### Commands
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
94
|
+
|---------|-------------|
|
|
95
|
+
| `ai-global` | Scan, merge and update symlinks (default) |
|
|
96
|
+
| `ai-global status` | Show symlink status |
|
|
97
|
+
| `ai-global list` | List supported tools |
|
|
98
|
+
| `ai-global backups` | List available backups |
|
|
99
|
+
| `ai-global restore <tool>` | Restore a tool's original config |
|
|
100
|
+
| `ai-global restore all` | Restore all tools |
|
|
101
|
+
| `ai-global skill <source>` | Add a skill (file or GitHub repo) |
|
|
102
|
+
| `ai-global agent <source>` | Add an agent |
|
|
103
|
+
| `ai-global rule <source>` | Add a rule |
|
|
104
|
+
| `ai-global command <source>` | Add a command |
|
|
105
|
+
| `ai-global prompt <source>` | Add a prompt |
|
|
106
|
+
| `ai-global upgrade` | Upgrade to latest version |
|
|
107
|
+
| `ai-global uninstall` | Completely remove ai-global |
|
|
108
|
+
| `ai-global version` | Show version |
|
|
109
|
+
| `ai-global help` | Show help |
|
|
110
|
+
|
|
111
|
+
### Add skill/agent/rule/command/prompt
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# From local file
|
|
115
|
+
ai-global skill react.md
|
|
116
|
+
ai-global agent coder.md
|
|
117
|
+
|
|
118
|
+
# From GitHub repo (clone all .md files)
|
|
119
|
+
ai-global skill user/repo
|
|
120
|
+
ai-global skill https://github.com/user/repo
|
|
121
|
+
|
|
122
|
+
# From GitHub subdirectory
|
|
123
|
+
ai-global skill user/repo/skills
|
|
124
|
+
|
|
125
|
+
# From GitHub single file
|
|
126
|
+
ai-global skill user/repo/skills/react.md
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## How it works
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
~/.ai-global/
|
|
133
|
+
├── instructions.md <- Shared instructions (edit this)
|
|
134
|
+
├── skills/ <- Shared skills (merged from all tools)
|
|
135
|
+
├── agents/ <- Shared agents
|
|
136
|
+
├── rules/ <- Shared rules
|
|
137
|
+
├── commands/ <- Shared slash commands
|
|
138
|
+
├── prompts/ <- Shared prompt templates
|
|
139
|
+
└── backups/ <- Original configs (backup)
|
|
140
|
+
|
|
141
|
+
Each AI tool's config directory contains symlinks:
|
|
142
|
+
|
|
143
|
+
~/.claude/
|
|
144
|
+
├── CLAUDE.md -> ~/.ai-global/instructions.md (symlink)
|
|
145
|
+
├── skills/ -> ~/.ai-global/skills/ (symlink)
|
|
146
|
+
└── commands/ -> ~/.ai-global/commands/ (symlink)
|
|
147
|
+
|
|
148
|
+
~/.cursor/
|
|
149
|
+
├── rules/global.md -> ~/.ai-global/instructions.md (symlink)
|
|
150
|
+
├── skills/ -> ~/.ai-global/skills/ (symlink)
|
|
151
|
+
└── prompts/ -> ~/.ai-global/prompts/ (symlink)
|
|
152
|
+
|
|
153
|
+
... and more tools
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Merge behavior
|
|
157
|
+
|
|
158
|
+
When you run `ai-global`, it merges items from all tools by filename:
|
|
159
|
+
|
|
160
|
+
- Cursor has skills: `react.md`, `typescript.md`
|
|
161
|
+
- Claude has skills: `typescript.md`, `python.md`
|
|
162
|
+
- Result in `~/.ai-global/skills/`: `react.md`, `typescript.md`, `python.md`
|
|
163
|
+
|
|
164
|
+
The first file found wins (dedup by filename).
|
|
165
|
+
|
|
166
|
+
## Uninstall
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
ai-global uninstall
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
This will:
|
|
173
|
+
1. Restore all tools to original configuration
|
|
174
|
+
2. Remove `~/.ai-global` directory
|
|
175
|
+
3. Remove `ai-global` command
|
|
176
|
+
|
|
177
|
+
If installed via npm:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
ai-global uninstall
|
|
181
|
+
npm uninstall -g ai-global
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Example instructions.md
|
|
185
|
+
|
|
186
|
+
```markdown
|
|
187
|
+
# AI Assistant Instructions
|
|
188
|
+
|
|
189
|
+
- Use pnpm as the package manager
|
|
190
|
+
- Reply in English
|
|
191
|
+
- Follow existing code style
|
|
192
|
+
- Use TypeScript with `type` for type definitions
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
package/ai-global
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# AI-Global: Unified AI Tools Configuration Manager
|
|
4
|
+
# https://github.com/nanxiaobei/ai-global
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
CONFIG_DIR="$HOME/.ai-global"
|
|
9
|
+
KNOWN_TOOLS_FILE="$CONFIG_DIR/.known-tools"
|
|
10
|
+
BACKUP_DIR="$CONFIG_DIR/backups"
|
|
11
|
+
INSTRUCTIONS_MD="$CONFIG_DIR/instructions.md"
|
|
12
|
+
SKILLS_DIR="$CONFIG_DIR/skills"
|
|
13
|
+
AGENTS_DIR="$CONFIG_DIR/agents"
|
|
14
|
+
RULES_DIR="$CONFIG_DIR/rules"
|
|
15
|
+
COMMANDS_DIR="$CONFIG_DIR/commands"
|
|
16
|
+
PROMPTS_DIR="$CONFIG_DIR/prompts"
|
|
17
|
+
|
|
18
|
+
# Version (read from package.json)
|
|
19
|
+
# Prefer node for correct JSON parsing; fallback to grep/sed; finally fallback to a default.
|
|
20
|
+
VERSION=""
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
PACKAGE_JSON="$SCRIPT_DIR/package.json"
|
|
23
|
+
|
|
24
|
+
if [[ -f "$PACKAGE_JSON" ]]; then
|
|
25
|
+
if command -v node >/dev/null 2>&1; then
|
|
26
|
+
VERSION=$(node -p "try{require(process.env.PACKAGE_JSON).version||''}catch(e){''}" 2>/dev/null PACKAGE_JSON="$PACKAGE_JSON")
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ -z "$VERSION" ]]; then
|
|
30
|
+
VERSION=$(grep '"version"' "$PACKAGE_JSON" 2>/dev/null | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
VERSION=${VERSION:-""}
|
|
35
|
+
|
|
36
|
+
# Colors
|
|
37
|
+
RED='\033[0;31m'
|
|
38
|
+
GREEN='\033[0;32m'
|
|
39
|
+
YELLOW='\033[0;33m'
|
|
40
|
+
BLUE='\033[0;34m'
|
|
41
|
+
CYAN='\033[0;36m'
|
|
42
|
+
NC='\033[0m'
|
|
43
|
+
|
|
44
|
+
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
45
|
+
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
46
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
47
|
+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
48
|
+
|
|
49
|
+
# Known AI tool patterns
|
|
50
|
+
# Format: dir|name|instr_file|skills_dir|agents_dir|rules_dir|commands_dir|prompts_dir
|
|
51
|
+
declare -a KNOWN_PATTERNS=(
|
|
52
|
+
".claude|Claude Code|CLAUDE.md|skills|.|.|commands|."
|
|
53
|
+
".cursor|Cursor|rules/global.md|skills|.|.|.|prompts"
|
|
54
|
+
".factory|Factory Droid|droids/global.md|.|.|.|.|."
|
|
55
|
+
".amp|Amp|instructions.md|.|.|.|.|."
|
|
56
|
+
".antigravity|Antigravity|CLAUDE.md|skills|.|.|commands|."
|
|
57
|
+
".gemini|Gemini CLI|GEMINI.md|skills|.|.|commands|."
|
|
58
|
+
".kiro|Kiro|steering/global.md|.|agents|.|.|."
|
|
59
|
+
".opencode|OpenCode|instructions.md|.|.|.|.|."
|
|
60
|
+
".qoder|Qoder|instructions.md|.|.|.|.|."
|
|
61
|
+
".qodo|Qodo|instructions.md|.|agents|.|.|."
|
|
62
|
+
".config/github-copilot|GitHub Copilot|instructions.md|.|.|.|.|."
|
|
63
|
+
".aider|Aider|.aider.conf.yml|.|.|.|.|."
|
|
64
|
+
".continue|Continue|config.json|.|.|rules|.|prompts"
|
|
65
|
+
".codeium|Codeium|config.json|.|.|.|.|."
|
|
66
|
+
".tabnine|TabNine|tabnine_config.json|.|.|.|.|."
|
|
67
|
+
".sourcegraph|Sourcegraph Cody|cody.json|.|.|.|commands|."
|
|
68
|
+
".codegpt|CodeGPT|settings.json|.|.|.|.|prompts"
|
|
69
|
+
".windsurf|Windsurf|instructions.md|skills|agents|rules|.|."
|
|
70
|
+
".trae|Trae|instructions.md|.|.|.|.|."
|
|
71
|
+
".melty|Melty|instructions.md|.|.|.|.|."
|
|
72
|
+
".void|Void|instructions.md|.|.|.|.|."
|
|
73
|
+
".roo|Roo Code|instructions.md|.|.|rules|.|."
|
|
74
|
+
".zed|Zed|settings.json|.|.|.|.|."
|
|
75
|
+
".cline|Cline|instructions.md|.|.|rules|.|prompts"
|
|
76
|
+
".aide|Aide|instructions.md|.|.|.|.|."
|
|
77
|
+
".pear|PearAI|instructions.md|.|.|.|.|."
|
|
78
|
+
".supermaven|Supermaven|config.json|.|.|.|.|."
|
|
79
|
+
".codestory|CodeStory|instructions.md|.|.|.|.|."
|
|
80
|
+
".double|Double|instructions.md|.|.|.|.|."
|
|
81
|
+
".blackbox|Blackbox AI|config.json|.|.|.|.|."
|
|
82
|
+
".amazonq|Amazon Q|instructions.md|.|.|.|.|."
|
|
83
|
+
".copilot-workspace|Copilot Workspace|instructions.md|.|.|.|.|."
|
|
84
|
+
".codex|OpenAI Codex|instructions.md|.|.|.|.|."
|
|
85
|
+
".goose|Goose AI|instructions.md|.|.|.|.|."
|
|
86
|
+
".mentat|Mentat|instructions.md|.|.|.|.|."
|
|
87
|
+
".gpt-engineer|GPT Engineer|instructions.md|.|.|.|.|prompts"
|
|
88
|
+
".smol|Smol Developer|instructions.md|.|.|.|.|prompts"
|
|
89
|
+
".config/opencode|OpenCode Config|instructions.md|.|.|.|.|."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Backup a file or directory
|
|
93
|
+
backup_item() {
|
|
94
|
+
local source="$1"
|
|
95
|
+
local tool_dir="$2"
|
|
96
|
+
local type="$3"
|
|
97
|
+
|
|
98
|
+
[[ ! -e "$source" ]] && return 0
|
|
99
|
+
[[ -L "$source" ]] && return 0
|
|
100
|
+
|
|
101
|
+
mkdir -p "$BACKUP_DIR"
|
|
102
|
+
|
|
103
|
+
local backup_name=$(echo "$tool_dir" | tr '/' '_')
|
|
104
|
+
local timestamp=$(date +%s)
|
|
105
|
+
local backup_path="$BACKUP_DIR/${backup_name}.${type}.${timestamp}"
|
|
106
|
+
|
|
107
|
+
if [[ -d "$source" ]]; then
|
|
108
|
+
cp -r "$source" "$backup_path" 2>/dev/null || return 0
|
|
109
|
+
else
|
|
110
|
+
cp "$source" "$backup_path" 2>/dev/null || return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
log_ok "Backed up: $source"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Merge items from a tool to shared directory (dedup by filename)
|
|
117
|
+
merge_items() {
|
|
118
|
+
local source_dir="$1"
|
|
119
|
+
local target_dir="$2"
|
|
120
|
+
local type="$3"
|
|
121
|
+
|
|
122
|
+
[[ ! -d "$source_dir" ]] && return
|
|
123
|
+
[[ -L "$source_dir" ]] && return
|
|
124
|
+
|
|
125
|
+
local merged_count=0
|
|
126
|
+
|
|
127
|
+
for item in "$source_dir"/*; do
|
|
128
|
+
[[ ! -e "$item" ]] && continue
|
|
129
|
+
local name=$(basename "$item")
|
|
130
|
+
local target="$target_dir/$name"
|
|
131
|
+
|
|
132
|
+
[[ -e "$target" ]] && continue
|
|
133
|
+
|
|
134
|
+
if [[ -d "$item" ]]; then
|
|
135
|
+
cp -r "$item" "$target"
|
|
136
|
+
else
|
|
137
|
+
cp "$item" "$target"
|
|
138
|
+
fi
|
|
139
|
+
((merged_count++))
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
if [[ $merged_count -gt 0 ]]; then
|
|
143
|
+
log_ok "Merged $merged_count $type from $source_dir"
|
|
144
|
+
fi
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Create symlink
|
|
148
|
+
create_symlink() {
|
|
149
|
+
local source="$1"
|
|
150
|
+
local target="$2"
|
|
151
|
+
|
|
152
|
+
[[ ! -e "$source" ]] && return
|
|
153
|
+
|
|
154
|
+
local target_dir=$(dirname "$target")
|
|
155
|
+
mkdir -p "$target_dir"
|
|
156
|
+
|
|
157
|
+
if [[ -e "$target" ]] && [[ ! -L "$target" ]]; then
|
|
158
|
+
local backup="${target}.backup.$(date +%Y%m%d%H%M%S)"
|
|
159
|
+
mv "$target" "$backup"
|
|
160
|
+
log_warn "Backed up: $target"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
[[ -L "$target" ]] && rm "$target"
|
|
164
|
+
ln -s "$source" "$target"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Count files in directory
|
|
168
|
+
count_files() {
|
|
169
|
+
local dir="$1"
|
|
170
|
+
if [[ -d "$dir" ]]; then
|
|
171
|
+
find "$dir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' '
|
|
172
|
+
else
|
|
173
|
+
echo "0"
|
|
174
|
+
fi
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Show symlink status
|
|
178
|
+
show_status() {
|
|
179
|
+
log_info "Symlink status:"
|
|
180
|
+
echo ""
|
|
181
|
+
|
|
182
|
+
if [[ ! -f "$KNOWN_TOOLS_FILE" ]]; then
|
|
183
|
+
log_info "No tools configured. Run 'ai-global' first."
|
|
184
|
+
return
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
echo -e "${BLUE}[instructions]${NC}"
|
|
188
|
+
while IFS= read -r dir_name; do
|
|
189
|
+
[[ -z "$dir_name" ]] && continue
|
|
190
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
191
|
+
IFS='|' read -r known_dir tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
192
|
+
if [[ "$dir_name" == "$known_dir" ]]; then
|
|
193
|
+
if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
|
|
194
|
+
local target="$HOME/$dir_name/$instr_file"
|
|
195
|
+
if [[ -L "$target" ]]; then
|
|
196
|
+
echo -e " ${GREEN}✓${NC} $target"
|
|
197
|
+
elif [[ -e "$target" ]]; then
|
|
198
|
+
echo -e " ${YELLOW}!${NC} $target (not a symlink)"
|
|
199
|
+
else
|
|
200
|
+
echo -e " ${RED}✗${NC} $target (missing)"
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
break
|
|
204
|
+
fi
|
|
205
|
+
done
|
|
206
|
+
done < "$KNOWN_TOOLS_FILE"
|
|
207
|
+
|
|
208
|
+
for type_name in skills agents rules commands prompts; do
|
|
209
|
+
echo -e "\n${BLUE}[$type_name]${NC}"
|
|
210
|
+
while IFS= read -r dir_name; do
|
|
211
|
+
[[ -z "$dir_name" ]] && continue
|
|
212
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
213
|
+
IFS='|' read -r known_dir tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
214
|
+
if [[ "$dir_name" == "$known_dir" ]]; then
|
|
215
|
+
local type_dir=""
|
|
216
|
+
case "$type_name" in
|
|
217
|
+
skills) type_dir="$skills" ;;
|
|
218
|
+
agents) type_dir="$agents" ;;
|
|
219
|
+
rules) type_dir="$rules" ;;
|
|
220
|
+
commands) type_dir="$commands" ;;
|
|
221
|
+
prompts) type_dir="$prompts" ;;
|
|
222
|
+
esac
|
|
223
|
+
if [[ "$type_dir" != "." ]]; then
|
|
224
|
+
local target="$HOME/$dir_name/$type_dir"
|
|
225
|
+
if [[ -L "$target" ]]; then
|
|
226
|
+
echo -e " ${GREEN}✓${NC} $target"
|
|
227
|
+
elif [[ -e "$target" ]]; then
|
|
228
|
+
echo -e " ${YELLOW}!${NC} $target (not a symlink)"
|
|
229
|
+
else
|
|
230
|
+
echo -e " ${RED}✗${NC} $target (missing)"
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
break
|
|
234
|
+
fi
|
|
235
|
+
done
|
|
236
|
+
done < "$KNOWN_TOOLS_FILE"
|
|
237
|
+
done
|
|
238
|
+
|
|
239
|
+
echo ""
|
|
240
|
+
log_info "Shared: skills=$(count_files "$SKILLS_DIR"), agents=$(count_files "$AGENTS_DIR"), rules=$(count_files "$RULES_DIR"), commands=$(count_files "$COMMANDS_DIR"), prompts=$(count_files "$PROMPTS_DIR")"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# List supported tools
|
|
244
|
+
list_supported() {
|
|
245
|
+
log_info "Supported AI tools:"
|
|
246
|
+
echo ""
|
|
247
|
+
printf " ${BLUE}%-22s${NC} %-20s %-6s %-6s %-6s %-6s %-6s %s\n" "Directory" "Tool" "Skills" "Agents" "Rules" "Cmds" "Prompts" "Status"
|
|
248
|
+
echo " ----------------------------------------------------------------------------------------------------------------"
|
|
249
|
+
|
|
250
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
251
|
+
IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
252
|
+
local full_path="$HOME/$dir_name"
|
|
253
|
+
|
|
254
|
+
local s="." a="." r="." c="." p="."
|
|
255
|
+
[[ "$skills" != "." ]] && s="✓"
|
|
256
|
+
[[ "$agents" != "." ]] && a="✓"
|
|
257
|
+
[[ "$rules" != "." ]] && r="✓"
|
|
258
|
+
[[ "$commands" != "." ]] && c="✓"
|
|
259
|
+
[[ "$prompts" != "." ]] && p="✓"
|
|
260
|
+
|
|
261
|
+
local status=""
|
|
262
|
+
if [[ -d "$full_path" ]]; then
|
|
263
|
+
status="${GREEN}Installed${NC}"
|
|
264
|
+
else
|
|
265
|
+
status="${YELLOW}Not found${NC}"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
printf " %-22s %-20s %-6s %-6s %-6s %-6s %-6s %b\n" "$dir_name" "$tool_name" "$s" "$a" "$r" "$c" "$p" "$status"
|
|
269
|
+
done
|
|
270
|
+
echo ""
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# List available backups
|
|
274
|
+
list_backups() {
|
|
275
|
+
log_info "Available backups:"
|
|
276
|
+
echo ""
|
|
277
|
+
|
|
278
|
+
if [[ ! -d "$BACKUP_DIR" ]] || [[ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]]; then
|
|
279
|
+
echo " No backups found"
|
|
280
|
+
echo ""
|
|
281
|
+
return
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
printf " ${BLUE}%-25s${NC} %-12s %s\n" "Tool" "Type" "Backup File"
|
|
285
|
+
echo " ----------------------------------------------------------------"
|
|
286
|
+
|
|
287
|
+
for backup in "$BACKUP_DIR"/*; do
|
|
288
|
+
[[ ! -e "$backup" ]] && continue
|
|
289
|
+
local filename=$(basename "$backup")
|
|
290
|
+
local tool_name=""
|
|
291
|
+
local type=""
|
|
292
|
+
|
|
293
|
+
if [[ "$filename" =~ ^(.+)\.([^.]+)\.([0-9]+)$ ]]; then
|
|
294
|
+
tool_name="${BASH_REMATCH[1]}"
|
|
295
|
+
type="${BASH_REMATCH[2]}"
|
|
296
|
+
else
|
|
297
|
+
tool_name="$filename"
|
|
298
|
+
type="unknown"
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
printf " %-25s %-12s %s\n" "$tool_name" "$type" "$filename"
|
|
302
|
+
done
|
|
303
|
+
echo ""
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Collect and merge instructions from all tools
|
|
307
|
+
collect_instructions() {
|
|
308
|
+
local merged_content=""
|
|
309
|
+
local found_count=0
|
|
310
|
+
|
|
311
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
312
|
+
IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
313
|
+
|
|
314
|
+
if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
|
|
315
|
+
local actual_path="$HOME/$dir_name/$instr_file"
|
|
316
|
+
[[ -L "$actual_path" ]] && continue
|
|
317
|
+
|
|
318
|
+
if [[ -f "$actual_path" ]]; then
|
|
319
|
+
local content=$(cat "$actual_path" 2>/dev/null)
|
|
320
|
+
if [[ -n "$content" ]]; then
|
|
321
|
+
if [[ $found_count -gt 0 ]]; then
|
|
322
|
+
merged_content+="\n\n---\n\n"
|
|
323
|
+
fi
|
|
324
|
+
merged_content+="# From $tool_name\n\n$content"
|
|
325
|
+
((found_count++))
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
done
|
|
330
|
+
|
|
331
|
+
if [[ $found_count -gt 0 ]]; then
|
|
332
|
+
echo -e "$merged_content" > "$INSTRUCTIONS_MD"
|
|
333
|
+
log_ok "Merged instructions from $found_count tool(s)"
|
|
334
|
+
else
|
|
335
|
+
cat > "$INSTRUCTIONS_MD" << 'EOF'
|
|
336
|
+
# AI Assistant Instructions
|
|
337
|
+
|
|
338
|
+
<!-- Add your instructions here. They will sync to all AI tools. -->
|
|
339
|
+
EOF
|
|
340
|
+
log_ok "Created: $INSTRUCTIONS_MD"
|
|
341
|
+
fi
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Update: scan, merge and link tools
|
|
345
|
+
update_tools() {
|
|
346
|
+
log_info "Scanning for AI tools..."
|
|
347
|
+
echo ""
|
|
348
|
+
|
|
349
|
+
mkdir -p "$SKILLS_DIR" "$AGENTS_DIR" "$RULES_DIR" "$COMMANDS_DIR" "$PROMPTS_DIR" "$BACKUP_DIR"
|
|
350
|
+
|
|
351
|
+
if [[ ! -f "$INSTRUCTIONS_MD" ]]; then
|
|
352
|
+
collect_instructions
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
> "$KNOWN_TOOLS_FILE"
|
|
356
|
+
|
|
357
|
+
local tool_count=0
|
|
358
|
+
|
|
359
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
360
|
+
IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
361
|
+
local full_path="$HOME/$dir_name"
|
|
362
|
+
|
|
363
|
+
if [[ -d "$full_path" ]]; then
|
|
364
|
+
log_ok "Found: $tool_name"
|
|
365
|
+
((tool_count++))
|
|
366
|
+
|
|
367
|
+
if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
|
|
368
|
+
local actual_path="$HOME/$dir_name/$instr_file"
|
|
369
|
+
backup_item "$actual_path" "$dir_name" "instructions"
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
for type_name in skills agents rules commands prompts; do
|
|
373
|
+
local type_dir=""
|
|
374
|
+
case "$type_name" in
|
|
375
|
+
skills) type_dir="$skills" ;;
|
|
376
|
+
agents) type_dir="$agents" ;;
|
|
377
|
+
rules) type_dir="$rules" ;;
|
|
378
|
+
commands) type_dir="$commands" ;;
|
|
379
|
+
prompts) type_dir="$prompts" ;;
|
|
380
|
+
esac
|
|
381
|
+
if [[ "$type_dir" != "." ]]; then
|
|
382
|
+
local path="$HOME/$dir_name/$type_dir"
|
|
383
|
+
backup_item "$path" "$dir_name" "$type_name"
|
|
384
|
+
local target_dir=""
|
|
385
|
+
case "$type_name" in
|
|
386
|
+
skills) target_dir="$SKILLS_DIR" ;;
|
|
387
|
+
agents) target_dir="$AGENTS_DIR" ;;
|
|
388
|
+
rules) target_dir="$RULES_DIR" ;;
|
|
389
|
+
commands) target_dir="$COMMANDS_DIR" ;;
|
|
390
|
+
prompts) target_dir="$PROMPTS_DIR" ;;
|
|
391
|
+
esac
|
|
392
|
+
merge_items "$path" "$target_dir" "$type_name"
|
|
393
|
+
fi
|
|
394
|
+
done
|
|
395
|
+
|
|
396
|
+
echo "$dir_name" >> "$KNOWN_TOOLS_FILE"
|
|
397
|
+
fi
|
|
398
|
+
done
|
|
399
|
+
|
|
400
|
+
if [[ $tool_count -eq 0 ]]; then
|
|
401
|
+
log_info "No AI tools found."
|
|
402
|
+
return
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
echo ""
|
|
406
|
+
log_info "Creating symlinks..."
|
|
407
|
+
|
|
408
|
+
while IFS= read -r dir_name; do
|
|
409
|
+
[[ -z "$dir_name" ]] && continue
|
|
410
|
+
|
|
411
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
412
|
+
IFS='|' read -r known_dir tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
413
|
+
|
|
414
|
+
if [[ "$dir_name" == "$known_dir" ]]; then
|
|
415
|
+
if [[ "$instr_file" != "." ]] && [[ "$instr_file" != *.json ]] && [[ "$instr_file" != *.yml ]]; then
|
|
416
|
+
local target="$HOME/$dir_name/$instr_file"
|
|
417
|
+
create_symlink "$INSTRUCTIONS_MD" "$target"
|
|
418
|
+
log_ok "$target -> instructions.md"
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
for type_name in skills agents rules commands prompts; do
|
|
422
|
+
local type_dir=""
|
|
423
|
+
local source_dir=""
|
|
424
|
+
case "$type_name" in
|
|
425
|
+
skills) type_dir="$skills"; source_dir="$SKILLS_DIR" ;;
|
|
426
|
+
agents) type_dir="$agents"; source_dir="$AGENTS_DIR" ;;
|
|
427
|
+
rules) type_dir="$rules"; source_dir="$RULES_DIR" ;;
|
|
428
|
+
commands) type_dir="$commands"; source_dir="$COMMANDS_DIR" ;;
|
|
429
|
+
prompts) type_dir="$prompts"; source_dir="$PROMPTS_DIR" ;;
|
|
430
|
+
esac
|
|
431
|
+
if [[ "$type_dir" != "." ]]; then
|
|
432
|
+
local target="$HOME/$dir_name/$type_dir"
|
|
433
|
+
create_symlink "$source_dir" "$target"
|
|
434
|
+
log_ok "$target -> $type_name/"
|
|
435
|
+
fi
|
|
436
|
+
done
|
|
437
|
+
|
|
438
|
+
break
|
|
439
|
+
fi
|
|
440
|
+
done
|
|
441
|
+
done < "$KNOWN_TOOLS_FILE"
|
|
442
|
+
|
|
443
|
+
echo ""
|
|
444
|
+
log_info "Done! Shared: skills=$(count_files "$SKILLS_DIR"), agents=$(count_files "$AGENTS_DIR"), rules=$(count_files "$RULES_DIR"), commands=$(count_files "$COMMANDS_DIR"), prompts=$(count_files "$PROMPTS_DIR")"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# Restore a single tool
|
|
448
|
+
restore_single_tool() {
|
|
449
|
+
local tool_name="$1"
|
|
450
|
+
local dir_name="$2"
|
|
451
|
+
local instr_file="$3"
|
|
452
|
+
local skills="$4"
|
|
453
|
+
local agents="$5"
|
|
454
|
+
local rules="$6"
|
|
455
|
+
local commands="$7"
|
|
456
|
+
local prompts="$8"
|
|
457
|
+
|
|
458
|
+
log_ok "Restoring: $tool_name"
|
|
459
|
+
local backup_name=$(echo "$dir_name" | tr '/' '_')
|
|
460
|
+
|
|
461
|
+
if [[ "$instr_file" != "." ]]; then
|
|
462
|
+
local target="$HOME/$dir_name/$instr_file"
|
|
463
|
+
if [[ -L "$target" ]]; then
|
|
464
|
+
rm "$target"
|
|
465
|
+
local backup_file=$(ls -t "$BACKUP_DIR"/${backup_name}.instructions.* 2>/dev/null | head -1)
|
|
466
|
+
[[ -f "$backup_file" ]] && cp "$backup_file" "$target"
|
|
467
|
+
fi
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
for type_name in skills agents rules commands prompts; do
|
|
471
|
+
local type_dir=""
|
|
472
|
+
case "$type_name" in
|
|
473
|
+
skills) type_dir="$skills" ;;
|
|
474
|
+
agents) type_dir="$agents" ;;
|
|
475
|
+
rules) type_dir="$rules" ;;
|
|
476
|
+
commands) type_dir="$commands" ;;
|
|
477
|
+
prompts) type_dir="$prompts" ;;
|
|
478
|
+
esac
|
|
479
|
+
if [[ "$type_dir" != "." ]]; then
|
|
480
|
+
local target="$HOME/$dir_name/$type_dir"
|
|
481
|
+
if [[ -L "$target" ]]; then
|
|
482
|
+
rm "$target"
|
|
483
|
+
local backup_file=$(ls -td "$BACKUP_DIR"/${backup_name}.${type_name}.* 2>/dev/null | head -1)
|
|
484
|
+
if [[ -d "$backup_file" ]]; then
|
|
485
|
+
cp -r "$backup_file" "$target"
|
|
486
|
+
else
|
|
487
|
+
mkdir -p "$target"
|
|
488
|
+
fi
|
|
489
|
+
fi
|
|
490
|
+
fi
|
|
491
|
+
done
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# Restore all tools
|
|
495
|
+
restore_all_tools() {
|
|
496
|
+
log_info "Restoring all tools..."
|
|
497
|
+
echo ""
|
|
498
|
+
|
|
499
|
+
if [[ ! -f "$KNOWN_TOOLS_FILE" ]]; then
|
|
500
|
+
log_info "No tools to restore"
|
|
501
|
+
return
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
local restored_count=0
|
|
505
|
+
|
|
506
|
+
while IFS= read -r dir_name; do
|
|
507
|
+
[[ -z "$dir_name" ]] && continue
|
|
508
|
+
|
|
509
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
510
|
+
IFS='|' read -r known_dir tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
511
|
+
|
|
512
|
+
if [[ "$dir_name" == "$known_dir" ]]; then
|
|
513
|
+
restore_single_tool "$tool_name" "$dir_name" "$instr_file" "$skills" "$agents" "$rules" "$commands" "$prompts"
|
|
514
|
+
((restored_count++))
|
|
515
|
+
break
|
|
516
|
+
fi
|
|
517
|
+
done
|
|
518
|
+
done < "$KNOWN_TOOLS_FILE"
|
|
519
|
+
|
|
520
|
+
rm -f "$KNOWN_TOOLS_FILE"
|
|
521
|
+
|
|
522
|
+
echo ""
|
|
523
|
+
log_info "Restored $restored_count tool(s). Run 'ai-global' to re-link."
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# Restore a specific tool
|
|
527
|
+
restore_tool() {
|
|
528
|
+
local tool_query="$1"
|
|
529
|
+
|
|
530
|
+
if [[ -z "$tool_query" ]]; then
|
|
531
|
+
log_error "Usage: ai-global restore <tool> or ai-global restore all"
|
|
532
|
+
echo ""
|
|
533
|
+
list_backups
|
|
534
|
+
return 1
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
if [[ "$tool_query" == "all" ]]; then
|
|
538
|
+
restore_all_tools
|
|
539
|
+
return
|
|
540
|
+
fi
|
|
541
|
+
|
|
542
|
+
local found=false
|
|
543
|
+
|
|
544
|
+
for pattern in "${KNOWN_PATTERNS[@]}"; do
|
|
545
|
+
IFS='|' read -r dir_name tool_name instr_file skills agents rules commands prompts <<< "$pattern"
|
|
546
|
+
local tool_lower=$(echo "$tool_name" | tr '[:upper:]' '[:lower:]')
|
|
547
|
+
local query_lower=$(echo "$tool_query" | tr '[:upper:]' '[:lower:]')
|
|
548
|
+
|
|
549
|
+
if [[ "$tool_lower" == *"$query_lower"* ]] || [[ "$dir_name" == *"$query_lower"* ]]; then
|
|
550
|
+
log_info "Restoring $tool_name..."
|
|
551
|
+
restore_single_tool "$tool_name" "$dir_name" "$instr_file" "$skills" "$agents" "$rules" "$commands" "$prompts"
|
|
552
|
+
|
|
553
|
+
if [[ -f "$KNOWN_TOOLS_FILE" ]]; then
|
|
554
|
+
grep -v "^$dir_name$" "$KNOWN_TOOLS_FILE" > "$KNOWN_TOOLS_FILE.tmp" 2>/dev/null || true
|
|
555
|
+
mv "$KNOWN_TOOLS_FILE.tmp" "$KNOWN_TOOLS_FILE"
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
log_info "$tool_name restored"
|
|
559
|
+
found=true
|
|
560
|
+
break
|
|
561
|
+
fi
|
|
562
|
+
done
|
|
563
|
+
|
|
564
|
+
if [[ "$found" == false ]]; then
|
|
565
|
+
log_error "Tool not found: $tool_query"
|
|
566
|
+
echo ""
|
|
567
|
+
echo "Use 'ai-global list' to see supported tools"
|
|
568
|
+
return 1
|
|
569
|
+
fi
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# Check if input is a GitHub reference
|
|
573
|
+
is_github_ref() {
|
|
574
|
+
local input="$1"
|
|
575
|
+
# Match: user/repo, https://github.com/user/repo, github.com/user/repo
|
|
576
|
+
if [[ "$input" =~ ^https?://github\.com/ ]] || \
|
|
577
|
+
[[ "$input" =~ ^github\.com/ ]] || \
|
|
578
|
+
[[ "$input" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+(/.*)?$ ]]; then
|
|
579
|
+
return 0
|
|
580
|
+
fi
|
|
581
|
+
return 1
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Parse GitHub reference to get owner, repo, and optional path
|
|
585
|
+
parse_github_ref() {
|
|
586
|
+
local input="$1"
|
|
587
|
+
|
|
588
|
+
# Remove https://github.com/ or github.com/ prefix
|
|
589
|
+
input="${input#https://github.com/}"
|
|
590
|
+
input="${input#http://github.com/}"
|
|
591
|
+
input="${input#github.com/}"
|
|
592
|
+
|
|
593
|
+
# Remove /blob/main/ or /blob/master/ or /tree/main/ etc for file/dir paths
|
|
594
|
+
input=$(echo "$input" | sed -E 's|/blob/[^/]+/|/|; s|/tree/[^/]+/|/|')
|
|
595
|
+
|
|
596
|
+
echo "$input"
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# Download from GitHub
|
|
600
|
+
download_from_github() {
|
|
601
|
+
local type="$1"
|
|
602
|
+
local ref="$2"
|
|
603
|
+
local target_dir="$3"
|
|
604
|
+
|
|
605
|
+
local parsed=$(parse_github_ref "$ref")
|
|
606
|
+
local owner=$(echo "$parsed" | cut -d'/' -f1)
|
|
607
|
+
local repo=$(echo "$parsed" | cut -d'/' -f2)
|
|
608
|
+
local path=$(echo "$parsed" | cut -d'/' -f3-)
|
|
609
|
+
|
|
610
|
+
if [[ -z "$owner" ]] || [[ -z "$repo" ]]; then
|
|
611
|
+
log_error "Invalid GitHub reference: $ref"
|
|
612
|
+
return 1
|
|
613
|
+
fi
|
|
614
|
+
|
|
615
|
+
# If path points to a specific file
|
|
616
|
+
if [[ -n "$path" ]] && [[ "$path" == *.md ]]; then
|
|
617
|
+
local filename=$(basename "$path")
|
|
618
|
+
local raw_url="https://raw.githubusercontent.com/$owner/$repo/main/$path"
|
|
619
|
+
|
|
620
|
+
log_info "Downloading: $raw_url"
|
|
621
|
+
|
|
622
|
+
if curl -fsSL "$raw_url" -o "$target_dir/$filename" 2>/dev/null; then
|
|
623
|
+
log_ok "Added $type: $target_dir/$filename"
|
|
624
|
+
else
|
|
625
|
+
# Try master branch
|
|
626
|
+
raw_url="https://raw.githubusercontent.com/$owner/$repo/master/$path"
|
|
627
|
+
if curl -fsSL "$raw_url" -o "$target_dir/$filename" 2>/dev/null; then
|
|
628
|
+
log_ok "Added $type: $target_dir/$filename"
|
|
629
|
+
else
|
|
630
|
+
log_error "Failed to download: $ref"
|
|
631
|
+
return 1
|
|
632
|
+
fi
|
|
633
|
+
fi
|
|
634
|
+
else
|
|
635
|
+
# Clone entire repo or subdirectory
|
|
636
|
+
local tmp_dir=$(mktemp -d)
|
|
637
|
+
local clone_url="https://github.com/$owner/$repo.git"
|
|
638
|
+
|
|
639
|
+
log_info "Cloning: $clone_url"
|
|
640
|
+
|
|
641
|
+
if git clone --depth 1 --single-branch "$clone_url" "$tmp_dir/$repo"; then
|
|
642
|
+
local source_dir="$tmp_dir/$repo"
|
|
643
|
+
[[ -n "$path" ]] && source_dir="$tmp_dir/$repo/$path"
|
|
644
|
+
|
|
645
|
+
if [[ -d "$source_dir" ]]; then
|
|
646
|
+
# Auto-detect type subdirectory (e.g., skills/, agents/, rules/)
|
|
647
|
+
if [[ -z "$path" ]]; then
|
|
648
|
+
local type_dirs=""
|
|
649
|
+
case "$type" in
|
|
650
|
+
skill) type_dirs="skills skill" ;;
|
|
651
|
+
agent) type_dirs="agents agent" ;;
|
|
652
|
+
rule) type_dirs="rules rule" ;;
|
|
653
|
+
command) type_dirs="commands command" ;;
|
|
654
|
+
prompt) type_dirs="prompts prompt" ;;
|
|
655
|
+
esac
|
|
656
|
+
|
|
657
|
+
# Prefer type subdirectory, fallback to root
|
|
658
|
+
for dir in $type_dirs; do
|
|
659
|
+
if [[ -d "$source_dir/$dir" ]]; then
|
|
660
|
+
source_dir="$source_dir/$dir"
|
|
661
|
+
log_info "Found $dir/ directory"
|
|
662
|
+
break
|
|
663
|
+
fi
|
|
664
|
+
done
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
local count=0
|
|
668
|
+
|
|
669
|
+
# Special case: many skill repos are structured as:
|
|
670
|
+
# skills/<skill-name>/SKILL.md
|
|
671
|
+
if [[ "$type" == "skill" ]] && [[ "$(basename "$source_dir")" == "skills" ]]; then
|
|
672
|
+
for d in "$source_dir"/*; do
|
|
673
|
+
[[ ! -d "$d" ]] && continue
|
|
674
|
+
local skill_md="$d/SKILL.md"
|
|
675
|
+
[[ ! -f "$skill_md" ]] && continue
|
|
676
|
+
|
|
677
|
+
local skill_name=$(basename "$d")
|
|
678
|
+
|
|
679
|
+
# Copy the entire skill directory so assets/examples are preserved
|
|
680
|
+
local target_skill_dir="$target_dir/$skill_name"
|
|
681
|
+
mkdir -p "$target_skill_dir"
|
|
682
|
+
cp -R "$d/" "$target_skill_dir/"
|
|
683
|
+
|
|
684
|
+
((count++))
|
|
685
|
+
done
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
# Fallback: copy top-level .md files from the selected source directory
|
|
689
|
+
if [[ $count -eq 0 ]]; then
|
|
690
|
+
for file in "$source_dir"/*.md; do
|
|
691
|
+
[[ ! -f "$file" ]] && continue
|
|
692
|
+
local filename=$(basename "$file")
|
|
693
|
+
cp "$file" "$target_dir/$filename"
|
|
694
|
+
((count++))
|
|
695
|
+
done
|
|
696
|
+
fi
|
|
697
|
+
|
|
698
|
+
if [[ $count -gt 0 ]]; then
|
|
699
|
+
log_ok "Added $count ${type}(s) from $owner/$repo"
|
|
700
|
+
else
|
|
701
|
+
# Show actual searched path
|
|
702
|
+
local searched_path="${source_dir#$tmp_dir/$repo}"
|
|
703
|
+
searched_path="${searched_path#/}"
|
|
704
|
+
log_warn "No skills found in $owner/$repo${searched_path:+/$searched_path}"
|
|
705
|
+
fi
|
|
706
|
+
else
|
|
707
|
+
log_error "Path not found: $path"
|
|
708
|
+
rm -rf "$tmp_dir"
|
|
709
|
+
return 1
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
rm -rf "$tmp_dir"
|
|
713
|
+
else
|
|
714
|
+
rm -rf "$tmp_dir"
|
|
715
|
+
log_error "Failed to clone: $clone_url"
|
|
716
|
+
return 1
|
|
717
|
+
fi
|
|
718
|
+
fi
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
# Add item to a type directory
|
|
722
|
+
add_item() {
|
|
723
|
+
local type="$1"
|
|
724
|
+
local input="$2"
|
|
725
|
+
|
|
726
|
+
if [[ -z "$input" ]]; then
|
|
727
|
+
log_error "Usage: ai-global $type <file|github-repo>"
|
|
728
|
+
echo ""
|
|
729
|
+
echo "Examples:"
|
|
730
|
+
echo " ai-global $type react.md"
|
|
731
|
+
echo " ai-global $type /path/to/file.md"
|
|
732
|
+
echo " ai-global $type user/repo"
|
|
733
|
+
echo " ai-global $type https://github.com/user/repo"
|
|
734
|
+
echo " ai-global $type user/repo/path/to/file.md"
|
|
735
|
+
return 1
|
|
736
|
+
fi
|
|
737
|
+
|
|
738
|
+
local target_dir=""
|
|
739
|
+
case "$type" in
|
|
740
|
+
skill) target_dir="$SKILLS_DIR" ;;
|
|
741
|
+
agent) target_dir="$AGENTS_DIR" ;;
|
|
742
|
+
rule) target_dir="$RULES_DIR" ;;
|
|
743
|
+
command) target_dir="$COMMANDS_DIR" ;;
|
|
744
|
+
prompt) target_dir="$PROMPTS_DIR" ;;
|
|
745
|
+
esac
|
|
746
|
+
|
|
747
|
+
mkdir -p "$target_dir"
|
|
748
|
+
|
|
749
|
+
# Check if it's a GitHub reference
|
|
750
|
+
if is_github_ref "$input"; then
|
|
751
|
+
download_from_github "$type" "$input" "$target_dir"
|
|
752
|
+
elif [[ -f "$input" ]]; then
|
|
753
|
+
# Local file
|
|
754
|
+
local basename=$(basename "$input")
|
|
755
|
+
cp "$input" "$target_dir/$basename"
|
|
756
|
+
log_ok "Added $type: $target_dir/$basename"
|
|
757
|
+
else
|
|
758
|
+
# Create new file
|
|
759
|
+
local target_file="$target_dir/$input"
|
|
760
|
+
if [[ ! "$input" == *.md ]]; then
|
|
761
|
+
target_file="$target_dir/${input}.md"
|
|
762
|
+
fi
|
|
763
|
+
touch "$target_file"
|
|
764
|
+
log_ok "Created $type: $target_file"
|
|
765
|
+
echo "Edit: $target_file"
|
|
766
|
+
fi
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
# Uninstall
|
|
770
|
+
uninstall() {
|
|
771
|
+
log_warn "This will:"
|
|
772
|
+
echo " 1. Restore all tools to original configuration"
|
|
773
|
+
echo " 2. Remove ~/.ai-global directory"
|
|
774
|
+
echo " 3. Remove ai-global from PATH"
|
|
775
|
+
echo ""
|
|
776
|
+
read -p "Are you sure? (y/N) " -r
|
|
777
|
+
echo ""
|
|
778
|
+
|
|
779
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
780
|
+
log_info "Cancelled"
|
|
781
|
+
return
|
|
782
|
+
fi
|
|
783
|
+
|
|
784
|
+
if [[ -f "$KNOWN_TOOLS_FILE" ]]; then
|
|
785
|
+
restore_all_tools
|
|
786
|
+
fi
|
|
787
|
+
|
|
788
|
+
[[ -L /usr/local/bin/ai-global ]] && rm -f /usr/local/bin/ai-global
|
|
789
|
+
[[ -L "$HOME/.local/bin/ai-global" ]] && rm -f "$HOME/.local/bin/ai-global"
|
|
790
|
+
|
|
791
|
+
rm -rf "$CONFIG_DIR"
|
|
792
|
+
|
|
793
|
+
log_ok "AI-Global uninstalled"
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
# Show version
|
|
797
|
+
show_version() {
|
|
798
|
+
echo "ai-global version $VERSION"
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
# Upgrade
|
|
802
|
+
upgrade() {
|
|
803
|
+
log_info "Checking for updates..."
|
|
804
|
+
|
|
805
|
+
local remote_version
|
|
806
|
+
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/')
|
|
807
|
+
|
|
808
|
+
if [[ -z "$remote_version" ]]; then
|
|
809
|
+
log_warn "Could not check for updates"
|
|
810
|
+
return 1
|
|
811
|
+
fi
|
|
812
|
+
|
|
813
|
+
if [[ "$remote_version" == "$VERSION" ]]; then
|
|
814
|
+
log_ok "Already at latest version ($VERSION)"
|
|
815
|
+
return 0
|
|
816
|
+
fi
|
|
817
|
+
|
|
818
|
+
log_info "Upgrading: $VERSION -> $remote_version"
|
|
819
|
+
|
|
820
|
+
local tmp_file=$(mktemp)
|
|
821
|
+
if curl -fsSL "https://raw.githubusercontent.com/nanxiaobei/ai-global/main/ai-global" -o "$tmp_file" 2>/dev/null; then
|
|
822
|
+
chmod +x "$tmp_file"
|
|
823
|
+
mv "$tmp_file" "$CONFIG_DIR/ai-global"
|
|
824
|
+
log_ok "Upgraded to v$remote_version"
|
|
825
|
+
else
|
|
826
|
+
rm -f "$tmp_file"
|
|
827
|
+
log_error "Failed to download update"
|
|
828
|
+
return 1
|
|
829
|
+
fi
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
# Show help
|
|
833
|
+
show_help() {
|
|
834
|
+
cat << EOF
|
|
835
|
+
AI-Global: Unified AI Tools Configuration Manager v$VERSION
|
|
836
|
+
|
|
837
|
+
Usage: ai-global [command]
|
|
838
|
+
|
|
839
|
+
Commands:
|
|
840
|
+
(default) Scan, merge and update symlinks
|
|
841
|
+
status Show symlink status
|
|
842
|
+
list List all supported AI tools
|
|
843
|
+
backups List available backups
|
|
844
|
+
restore <tool> Restore a tool's original config
|
|
845
|
+
restore all Restore all tools
|
|
846
|
+
skill <source> Add a skill (file, GitHub repo, or new)
|
|
847
|
+
agent <source> Add an agent
|
|
848
|
+
rule <source> Add a rule
|
|
849
|
+
command <source> Add a command
|
|
850
|
+
prompt <source> Add a prompt
|
|
851
|
+
upgrade Upgrade ai-global to latest version
|
|
852
|
+
uninstall Completely remove ai-global
|
|
853
|
+
version Show version
|
|
854
|
+
help Show this help
|
|
855
|
+
|
|
856
|
+
Shared directories:
|
|
857
|
+
~/.ai-global/instructions.md Instructions for all tools
|
|
858
|
+
~/.ai-global/skills/ Reusable skills
|
|
859
|
+
~/.ai-global/agents/ Custom agents
|
|
860
|
+
~/.ai-global/rules/ Code rules
|
|
861
|
+
~/.ai-global/commands/ Slash commands
|
|
862
|
+
~/.ai-global/prompts/ Prompt templates
|
|
863
|
+
|
|
864
|
+
Examples:
|
|
865
|
+
ai-global # Scan, merge and update symlinks
|
|
866
|
+
ai-global skill react.md # Add a local skill file
|
|
867
|
+
ai-global skill user/repo # Add skills from GitHub repo (auto-detects skills/ dir)
|
|
868
|
+
ai-global agent coder.md # Add an agent
|
|
869
|
+
ai-global restore claude # Restore Claude's config
|
|
870
|
+
|
|
871
|
+
Note: When adding from GitHub, it auto-detects type subdirectories
|
|
872
|
+
(e.g., skills/, agents/, rules/) if they exist.
|
|
873
|
+
|
|
874
|
+
EOF
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
# Main
|
|
878
|
+
main() {
|
|
879
|
+
local cmd="${1:-update}"
|
|
880
|
+
|
|
881
|
+
if [[ "$1" == "-v" ]] || [[ "$1" == "--version" ]] || [[ "$1" == "version" ]]; then
|
|
882
|
+
show_version
|
|
883
|
+
exit 0
|
|
884
|
+
fi
|
|
885
|
+
|
|
886
|
+
if [[ ! -f "$KNOWN_TOOLS_FILE" ]]; then
|
|
887
|
+
case "$cmd" in
|
|
888
|
+
help|--help|-h) show_help; exit 0 ;;
|
|
889
|
+
list) list_supported; exit 0 ;;
|
|
890
|
+
version|-v|--version) show_version; exit 0 ;;
|
|
891
|
+
skill|agent|rule|command|prompt|restore|status|backups)
|
|
892
|
+
log_info "No tools configured yet. Running initial scan..."
|
|
893
|
+
update_tools
|
|
894
|
+
[[ "$cmd" == "skill" || "$cmd" == "agent" || "$cmd" == "rule" || "$cmd" == "command" || "$cmd" == "prompt" ]] || exit 0
|
|
895
|
+
;;
|
|
896
|
+
*) cmd="update" ;;
|
|
897
|
+
esac
|
|
898
|
+
fi
|
|
899
|
+
|
|
900
|
+
case "$cmd" in
|
|
901
|
+
update) update_tools ;;
|
|
902
|
+
status) show_status ;;
|
|
903
|
+
list) list_supported ;;
|
|
904
|
+
backups) list_backups ;;
|
|
905
|
+
restore) restore_tool "$2" ;;
|
|
906
|
+
skill) add_item "skill" "$2" ;;
|
|
907
|
+
agent) add_item "agent" "$2" ;;
|
|
908
|
+
rule) add_item "rule" "$2" ;;
|
|
909
|
+
command) add_item "command" "$2" ;;
|
|
910
|
+
prompt) add_item "prompt" "$2" ;;
|
|
911
|
+
upgrade) upgrade ;;
|
|
912
|
+
uninstall) uninstall ;;
|
|
913
|
+
version|-v|--version) show_version ;;
|
|
914
|
+
help|--help|-h) show_help ;;
|
|
915
|
+
*) log_error "Unknown command: $cmd"; show_help; exit 1 ;;
|
|
916
|
+
esac
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
main "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-global",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Unified configuration manager for AI coding assistants",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ai-global": "ai-global"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node scripts/postinstall.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"claude",
|
|
14
|
+
"cursor",
|
|
15
|
+
"copilot",
|
|
16
|
+
"gemini",
|
|
17
|
+
"coding-assistant",
|
|
18
|
+
"config",
|
|
19
|
+
"symlink"
|
|
20
|
+
],
|
|
21
|
+
"author": "nanxiaobei",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/nanxiaobei/ai-global.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/nanxiaobei/ai-global#readme",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=14"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"darwin",
|
|
33
|
+
"linux"
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"ai-global",
|
|
37
|
+
"scripts/"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const configDir = path.join(os.homedir(), '.ai-global');
|
|
8
|
+
|
|
9
|
+
// Create config directory
|
|
10
|
+
if (!fs.existsSync(configDir)) {
|
|
11
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log('\x1b[32m[OK]\x1b[0m AI-Link installed!');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log('Run \x1b[36mai-link\x1b[0m to scan and link all your AI tools');
|
|
18
|
+
console.log('');
|