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