aidevops 2.172.17 → 2.172.19
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/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +2 -1
- package/scripts/npm-postinstall.cjs +13 -2
- package/setup-modules/agent-deploy.sh +627 -0
- package/setup-modules/config.sh +183 -0
- package/setup-modules/core.sh +572 -0
- package/setup-modules/mcp-setup.sh +766 -0
- package/setup-modules/migrations.sh +961 -0
- package/setup-modules/plugins.sh +588 -0
- package/setup-modules/shell-env.sh +892 -0
- package/setup-modules/tool-install.sh +1373 -0
- package/setup.sh +1 -1
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Migration functions: migrate_* and cleanup_* functions
|
|
3
|
+
# Part of aidevops setup.sh modularization (t316.3)
|
|
4
|
+
|
|
5
|
+
# Shell safety baseline
|
|
6
|
+
set -Eeuo pipefail
|
|
7
|
+
IFS=$'\n\t'
|
|
8
|
+
# shellcheck disable=SC2154 # rc is assigned by $? in the trap string
|
|
9
|
+
trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
|
|
10
|
+
shopt -s inherit_errexit 2>/dev/null || true
|
|
11
|
+
|
|
12
|
+
cleanup_deprecated_paths() {
|
|
13
|
+
local agents_dir="$HOME/.aidevops/agents"
|
|
14
|
+
local cleaned=0
|
|
15
|
+
|
|
16
|
+
# List of deprecated paths (add new ones here when reorganizing)
|
|
17
|
+
local deprecated_paths=(
|
|
18
|
+
# v2.40.7: wordpress moved from root to tools/wordpress
|
|
19
|
+
"$agents_dir/wordpress.md"
|
|
20
|
+
"$agents_dir/wordpress"
|
|
21
|
+
# v2.41.0: build-agent and build-mcp moved from root to tools/
|
|
22
|
+
"$agents_dir/build-agent.md"
|
|
23
|
+
"$agents_dir/build-agent"
|
|
24
|
+
"$agents_dir/build-mcp.md"
|
|
25
|
+
"$agents_dir/build-mcp"
|
|
26
|
+
# v2.93.3: moltbot renamed to openclaw (formerly clawdbot)
|
|
27
|
+
"$agents_dir/tools/ai-assistants/clawdbot.md"
|
|
28
|
+
"$agents_dir/tools/ai-assistants/moltbot.md"
|
|
29
|
+
# Removed non-OpenCode AI tool docs (focus on OpenCode only)
|
|
30
|
+
"$agents_dir/tools/ai-assistants/windsurf.md"
|
|
31
|
+
"$agents_dir/tools/ai-assistants/configuration.md"
|
|
32
|
+
"$agents_dir/tools/ai-assistants/status.md"
|
|
33
|
+
# Removed oh-my-opencode integration (no longer supported)
|
|
34
|
+
"$agents_dir/tools/opencode/oh-my-opencode.md"
|
|
35
|
+
# t199.8: youtube moved from root to content/distribution/youtube/
|
|
36
|
+
"$agents_dir/youtube.md"
|
|
37
|
+
"$agents_dir/youtube"
|
|
38
|
+
# osgrep removed — disproportionate CPU/disk cost vs rg + LLM comprehension
|
|
39
|
+
"$agents_dir/tools/context/osgrep.md"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
for path in "${deprecated_paths[@]}"; do
|
|
43
|
+
if [[ -e "$path" ]]; then
|
|
44
|
+
rm -rf "$path"
|
|
45
|
+
((++cleaned))
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
if [[ $cleaned -gt 0 ]]; then
|
|
50
|
+
print_info "Cleaned up $cleaned deprecated agent path(s)"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Remove oh-my-opencode remnants (no longer supported) — but respect user preference.
|
|
54
|
+
# Default: preserve user files. Override with --overwrite flag or settings.json.
|
|
55
|
+
# See: ~/.config/aidevops/settings.json { "preserve_oh_my_opencode": true }
|
|
56
|
+
local omo_config="$HOME/.config/opencode/oh-my-opencode.json"
|
|
57
|
+
if [[ -f "$omo_config" ]]; then
|
|
58
|
+
if should_overwrite_user_file "preserve_oh_my_opencode" "oh-my-opencode config ($omo_config)"; then
|
|
59
|
+
rm -f "$omo_config"
|
|
60
|
+
print_info "Removed oh-my-opencode config"
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Remove osgrep — disproportionate CPU/disk cost (74GB indexes, 4 CPU cores on startup)
|
|
65
|
+
# rg + fd + LLM comprehension covers the same ground at zero resource cost
|
|
66
|
+
cleanup_osgrep
|
|
67
|
+
|
|
68
|
+
# Remove oh-my-opencode from plugin array if present — guarded by same setting
|
|
69
|
+
local opencode_config
|
|
70
|
+
opencode_config=$(find_opencode_config 2>/dev/null) || true
|
|
71
|
+
if [[ -n "$opencode_config" ]] && [[ -f "$opencode_config" ]] && command -v jq &>/dev/null; then
|
|
72
|
+
if jq -e '.plugin | index("oh-my-opencode")' "$opencode_config" >/dev/null 2>&1; then
|
|
73
|
+
if should_overwrite_user_file "preserve_oh_my_opencode" "oh-my-opencode plugin entry in OpenCode config"; then
|
|
74
|
+
local tmp_file
|
|
75
|
+
tmp_file=$(mktemp)
|
|
76
|
+
trap 'rm -f "${tmp_file:-}"' RETURN
|
|
77
|
+
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' "$opencode_config" >"$tmp_file" && mv "$tmp_file" "$opencode_config"
|
|
78
|
+
print_info "Removed oh-my-opencode from OpenCode plugin list"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Remove osgrep completely — one-time cleanup for all aidevops users
|
|
87
|
+
# osgrep consumed 74GB disk (lancedb indexes) and 4 CPU cores on startup.
|
|
88
|
+
# rg + fd + LLM comprehension covers the same ground at zero resource cost.
|
|
89
|
+
cleanup_osgrep() {
|
|
90
|
+
local cleaned=false
|
|
91
|
+
|
|
92
|
+
# 0. Kill running osgrep processes first (MCP servers, indexers)
|
|
93
|
+
# These are Node.js processes already loaded in memory — removing the
|
|
94
|
+
# binary and data won't stop them, and they may try to rebuild indexes.
|
|
95
|
+
if pgrep -f 'osgrep' >/dev/null 2>&1; then
|
|
96
|
+
print_info "Killing running osgrep processes..."
|
|
97
|
+
pkill -f 'osgrep' 2>/dev/null || true
|
|
98
|
+
# Give processes a moment to exit gracefully
|
|
99
|
+
sleep 1
|
|
100
|
+
# Force-kill any stragglers
|
|
101
|
+
pkill -9 -f 'osgrep' 2>/dev/null || true
|
|
102
|
+
cleaned=true
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# 1. Uninstall npm package (global)
|
|
106
|
+
if command -v osgrep &>/dev/null; then
|
|
107
|
+
print_info "Removing osgrep npm package..."
|
|
108
|
+
npm uninstall -g osgrep >/dev/null 2>&1 || true
|
|
109
|
+
cleaned=true
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# 2. Remove indexes, models, and config (~74GB)
|
|
113
|
+
if [[ -d "$HOME/.osgrep" ]]; then
|
|
114
|
+
print_info "Removing osgrep data directory (~74GB indexes)..."
|
|
115
|
+
rm -rf "$HOME/.osgrep"
|
|
116
|
+
cleaned=true
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# 3. Remove osgrep from OpenCode MCP config
|
|
120
|
+
local opencode_config
|
|
121
|
+
opencode_config=$(find_opencode_config 2>/dev/null) || true
|
|
122
|
+
if [[ -n "$opencode_config" ]] && [[ -f "$opencode_config" ]] && command -v jq &>/dev/null; then
|
|
123
|
+
if jq -e '.mcp["osgrep"]' "$opencode_config" >/dev/null 2>&1; then
|
|
124
|
+
local tmp_file
|
|
125
|
+
tmp_file=$(mktemp)
|
|
126
|
+
if jq 'del(.mcp["osgrep"]) | del(.tools["osgrep_*"])' "$opencode_config" >"$tmp_file" 2>/dev/null; then
|
|
127
|
+
mv "$tmp_file" "$opencode_config"
|
|
128
|
+
print_info "Removed osgrep from OpenCode MCP config"
|
|
129
|
+
else
|
|
130
|
+
rm -f "$tmp_file"
|
|
131
|
+
fi
|
|
132
|
+
cleaned=true
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# 4. Remove osgrep from Claude Code settings
|
|
137
|
+
local claude_settings="$HOME/.claude/settings.json"
|
|
138
|
+
if [[ -f "$claude_settings" ]] && command -v jq &>/dev/null; then
|
|
139
|
+
if jq -e '.mcpServers["osgrep"] // .enabledPlugins["osgrep@osgrep"]' "$claude_settings" >/dev/null 2>&1; then
|
|
140
|
+
local tmp_file
|
|
141
|
+
tmp_file=$(mktemp)
|
|
142
|
+
if jq 'del(.mcpServers["osgrep"]) | del(.enabledPlugins["osgrep@osgrep"])' "$claude_settings" >"$tmp_file" 2>/dev/null; then
|
|
143
|
+
mv "$tmp_file" "$claude_settings"
|
|
144
|
+
print_info "Removed osgrep from Claude Code settings"
|
|
145
|
+
else
|
|
146
|
+
rm -f "$tmp_file"
|
|
147
|
+
fi
|
|
148
|
+
cleaned=true
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# 5. Remove per-repo .osgrep directories in registered repos
|
|
153
|
+
local repos_file="$HOME/.config/aidevops/repos.json"
|
|
154
|
+
if [[ -f "$repos_file" ]] && command -v jq &>/dev/null; then
|
|
155
|
+
while IFS= read -r repo_path; do
|
|
156
|
+
[[ -z "$repo_path" ]] && continue
|
|
157
|
+
[[ ! -d "$repo_path" ]] && continue
|
|
158
|
+
if [[ -d "$repo_path/.osgrep" ]]; then
|
|
159
|
+
rm -rf "$repo_path/.osgrep"
|
|
160
|
+
fi
|
|
161
|
+
done < <(jq -r '.[]' "$repos_file" 2>/dev/null)
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
if [[ "$cleaned" == "true" ]]; then
|
|
165
|
+
print_success "osgrep removed (freed CPU cores and disk space)"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Remove stale bun-installed opencode if npm version exists (v2.123.5)
|
|
172
|
+
# Prior to v2.123.1, tool-version-check.sh used `bun install -g opencode-ai`.
|
|
173
|
+
# This left a binary at ~/.bun/bin/opencode that shadows the npm install
|
|
174
|
+
# if ~/.bun/bin is earlier in PATH than the npm bin directory.
|
|
175
|
+
cleanup_stale_bun_opencode() {
|
|
176
|
+
local bun_opencode="$HOME/.bun/bin/opencode"
|
|
177
|
+
local bun_modules="$HOME/.bun/install/global/node_modules/opencode-ai"
|
|
178
|
+
|
|
179
|
+
# Only clean up if the stale bun binary exists
|
|
180
|
+
if [[ ! -f "$bun_opencode" ]] && [[ ! -d "$bun_modules" ]]; then
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# Only clean up if npm version is installed (don't leave user without opencode)
|
|
185
|
+
local npm_opencode
|
|
186
|
+
npm_opencode=$(npm list -g opencode-ai --json 2>/dev/null | grep -c '"opencode-ai"' || true)
|
|
187
|
+
if [[ "$npm_opencode" -eq 0 ]]; then
|
|
188
|
+
# npm version not installed — install it first, then clean up bun
|
|
189
|
+
if command -v npm >/dev/null 2>&1; then
|
|
190
|
+
print_info "Installing opencode via npm (replacing bun install)..."
|
|
191
|
+
npm_global_install "opencode-ai" >/dev/null 2>&1 || true
|
|
192
|
+
else
|
|
193
|
+
# Can't install npm version — leave bun version in place
|
|
194
|
+
return 0
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Remove stale bun binary and modules
|
|
199
|
+
if [[ -f "$bun_opencode" ]]; then
|
|
200
|
+
rm -f "$bun_opencode"
|
|
201
|
+
print_info "Removed stale bun opencode binary: $bun_opencode"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
if [[ -d "$bun_modules" ]]; then
|
|
205
|
+
rm -rf "$bun_modules"
|
|
206
|
+
print_info "Removed stale bun opencode modules: $bun_modules"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
print_success "Cleaned up stale bun opencode install (npm version is canonical)"
|
|
210
|
+
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Migrate .agent -> .agents in user projects and local config
|
|
215
|
+
# v2.104.0: Industry converging on .agents/ folder convention (aligning with AGENTS.md)
|
|
216
|
+
# This migrates:
|
|
217
|
+
# 1. .agent symlinks in user projects -> .agents
|
|
218
|
+
# 2. .agent/loop-state/ -> .agents/loop-state/ in user projects
|
|
219
|
+
# 3. .gitignore entries in user projects
|
|
220
|
+
# 4. References in user's AI assistant configs
|
|
221
|
+
# 5. References in ~/.aidevops/ config files
|
|
222
|
+
migrate_agent_to_agents_folder() {
|
|
223
|
+
print_info "Checking for .agent -> .agents migration..."
|
|
224
|
+
|
|
225
|
+
local migrated=0
|
|
226
|
+
|
|
227
|
+
# 1. Migrate .agent symlinks in registered repos
|
|
228
|
+
local repos_file="$HOME/.config/aidevops/repos.json"
|
|
229
|
+
if [[ -f "$repos_file" ]] && command -v jq &>/dev/null; then
|
|
230
|
+
while IFS= read -r repo_path; do
|
|
231
|
+
[[ -z "$repo_path" ]] && continue
|
|
232
|
+
[[ ! -d "$repo_path" ]] && continue
|
|
233
|
+
|
|
234
|
+
# Migrate legacy .agent symlink/directory to .agents real directory
|
|
235
|
+
if [[ -L "$repo_path/.agent" ]]; then
|
|
236
|
+
rm -f "$repo_path/.agent"
|
|
237
|
+
if [[ ! -d "$repo_path/.agents" ]]; then
|
|
238
|
+
mkdir -p "$repo_path/.agents"
|
|
239
|
+
fi
|
|
240
|
+
print_info " Removed legacy .agent symlink in $(basename "$repo_path")"
|
|
241
|
+
((++migrated))
|
|
242
|
+
elif [[ -d "$repo_path/.agent" && ! -L "$repo_path/.agent" ]]; then
|
|
243
|
+
# Real directory (not symlink) - rename it
|
|
244
|
+
if [[ ! -e "$repo_path/.agents" ]]; then
|
|
245
|
+
mv "$repo_path/.agent" "$repo_path/.agents"
|
|
246
|
+
print_info " Renamed directory: $repo_path/.agent -> .agents"
|
|
247
|
+
((++migrated))
|
|
248
|
+
fi
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# Migrate legacy .agents symlink to real directory
|
|
252
|
+
if [[ -L "$repo_path/.agents" ]]; then
|
|
253
|
+
rm -f "$repo_path/.agents"
|
|
254
|
+
mkdir -p "$repo_path/.agents"
|
|
255
|
+
print_info " Replaced .agents symlink with real directory in $(basename "$repo_path")"
|
|
256
|
+
((++migrated))
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# Update .gitignore: remove legacy bare ".agents" (now tracked),
|
|
260
|
+
# add runtime artifact ignores, migrate .agent/ paths.
|
|
261
|
+
# SKIP in non-interactive mode (e.g. auto-update cron) to avoid
|
|
262
|
+
# leaving uncommitted changes in user repos (issue #2570 bug 1).
|
|
263
|
+
local gitignore="$repo_path/.gitignore"
|
|
264
|
+
if [[ "${NON_INTERACTIVE:-false}" == "true" ]]; then
|
|
265
|
+
if [[ -f "$gitignore" ]]; then
|
|
266
|
+
local needs_gitignore_update=false
|
|
267
|
+
if grep -q "^\.agents$" "$gitignore" 2>/dev/null ||
|
|
268
|
+
grep -q "^\.agent$" "$gitignore" 2>/dev/null ||
|
|
269
|
+
grep -q "^\.agent/loop-state/" "$gitignore" 2>/dev/null ||
|
|
270
|
+
! grep -q "^\.agents/loop-state/" "$gitignore" 2>/dev/null; then
|
|
271
|
+
needs_gitignore_update=true
|
|
272
|
+
fi
|
|
273
|
+
if [[ "$needs_gitignore_update" == "true" ]]; then
|
|
274
|
+
print_warning " $(basename "$repo_path")/.gitignore needs migration (skipped in non-interactive mode)"
|
|
275
|
+
print_info " Run 'aidevops init' in $(basename "$repo_path") or 'setup.sh -i' to apply"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
else
|
|
279
|
+
if [[ -f "$gitignore" ]]; then
|
|
280
|
+
# Remove legacy bare ".agents" and ".agent" entries (added by older versions)
|
|
281
|
+
# .agents/ is now a real committed directory, not a symlink to ignore
|
|
282
|
+
if grep -q "^\.agents$" "$gitignore" 2>/dev/null; then
|
|
283
|
+
sed -i '' '/^\.agents$/d' "$gitignore" 2>/dev/null ||
|
|
284
|
+
sed -i '/^\.agents$/d' "$gitignore" 2>/dev/null || true
|
|
285
|
+
print_info " Removed legacy bare .agents from .gitignore in $(basename "$repo_path")"
|
|
286
|
+
fi
|
|
287
|
+
if grep -q "^\.agent$" "$gitignore" 2>/dev/null; then
|
|
288
|
+
sed -i '' '/^\.agent$/d' "$gitignore" 2>/dev/null ||
|
|
289
|
+
sed -i '/^\.agent$/d' "$gitignore" 2>/dev/null || true
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# Migrate .agent/loop-state/ -> .agents/loop-state/
|
|
293
|
+
if grep -q "^\.agent/loop-state/" "$gitignore" 2>/dev/null; then
|
|
294
|
+
sed -i '' 's|^\.agent/loop-state/|.agents/loop-state/|' "$gitignore" 2>/dev/null ||
|
|
295
|
+
sed -i 's|^\.agent/loop-state/|.agents/loop-state/|' "$gitignore" 2>/dev/null || true
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# Add runtime artifact ignores if not present
|
|
299
|
+
if ! grep -q "^\.agents/loop-state/" "$gitignore" 2>/dev/null; then
|
|
300
|
+
{
|
|
301
|
+
echo ""
|
|
302
|
+
echo "# aidevops runtime artifacts"
|
|
303
|
+
echo ".agents/loop-state/"
|
|
304
|
+
echo ".agents/tmp/"
|
|
305
|
+
echo ".agents/memory/"
|
|
306
|
+
} >>"$gitignore"
|
|
307
|
+
print_info " Added .agents/ runtime artifact ignores in $(basename "$repo_path")"
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
fi
|
|
311
|
+
done < <(jq -r '.initialized_repos[].path' "$repos_file" 2>/dev/null)
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# 2. Also scan ~/Git/ for any .agent symlinks or directories not in repos.json
|
|
315
|
+
if [[ -d "$HOME/Git" ]]; then
|
|
316
|
+
while IFS= read -r -d '' agent_path; do
|
|
317
|
+
local repo_dir
|
|
318
|
+
repo_dir=$(dirname "$agent_path")
|
|
319
|
+
|
|
320
|
+
if [[ -L "$agent_path" ]]; then
|
|
321
|
+
# Symlink: remove and create real directory
|
|
322
|
+
rm -f "$agent_path"
|
|
323
|
+
if [[ ! -d "$repo_dir/.agents" ]]; then
|
|
324
|
+
mkdir -p "$repo_dir/.agents"
|
|
325
|
+
fi
|
|
326
|
+
print_info " Removed legacy .agent symlink: $agent_path"
|
|
327
|
+
((++migrated))
|
|
328
|
+
elif [[ -d "$agent_path" ]]; then
|
|
329
|
+
# Directory: rename to .agents if .agents doesn't exist
|
|
330
|
+
if [[ ! -e "$repo_dir/.agents" ]]; then
|
|
331
|
+
mv "$agent_path" "$repo_dir/.agents"
|
|
332
|
+
print_info " Renamed directory: $agent_path -> .agents"
|
|
333
|
+
((++migrated))
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
done < <(find "$HOME/Git" -maxdepth 3 -name ".agent" \( -type l -o -type d \) -print0 2>/dev/null)
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# 3. Update AI assistant config files that reference .agent/
|
|
340
|
+
local ai_config_files=(
|
|
341
|
+
"$HOME/.config/opencode/agent/AGENTS.md"
|
|
342
|
+
"$HOME/.config/Claude/AGENTS.md"
|
|
343
|
+
"$HOME/.claude/commands/AGENTS.md"
|
|
344
|
+
"$HOME/.opencode/AGENTS.md"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
for config_file in "${ai_config_files[@]}"; do
|
|
348
|
+
if [[ -f "$config_file" ]]; then
|
|
349
|
+
if grep -q '\.agent/' "$config_file" 2>/dev/null; then
|
|
350
|
+
sed -i '' 's|\.agent/|.agents/|g' "$config_file" 2>/dev/null ||
|
|
351
|
+
sed -i 's|\.agent/|.agents/|g' "$config_file" 2>/dev/null || true
|
|
352
|
+
print_info " Updated references in $config_file"
|
|
353
|
+
((++migrated))
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
done
|
|
357
|
+
|
|
358
|
+
# 4. Update session greeting cache if it references .agent/
|
|
359
|
+
local greeting_cache="$HOME/.aidevops/cache/session-greeting.txt"
|
|
360
|
+
if [[ -f "$greeting_cache" ]]; then
|
|
361
|
+
if grep -q '\.agent/' "$greeting_cache" 2>/dev/null; then
|
|
362
|
+
sed -i '' 's|\.agent/|.agents/|g' "$greeting_cache" 2>/dev/null ||
|
|
363
|
+
sed -i 's|\.agent/|.agents/|g' "$greeting_cache" 2>/dev/null || true
|
|
364
|
+
fi
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [[ $migrated -gt 0 ]]; then
|
|
368
|
+
print_success "Migrated $migrated .agent -> .agents reference(s)"
|
|
369
|
+
else
|
|
370
|
+
print_info "No .agent -> .agents migration needed"
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
return 0
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Remove deprecated MCP entries from opencode.json
|
|
377
|
+
# These MCPs have been replaced by curl-based subagents (zero context cost)
|
|
378
|
+
cleanup_deprecated_mcps() {
|
|
379
|
+
local opencode_config
|
|
380
|
+
opencode_config=$(find_opencode_config) || return 0
|
|
381
|
+
|
|
382
|
+
if [[ ! -f "$opencode_config" ]]; then
|
|
383
|
+
return 0
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
if ! command -v jq &>/dev/null; then
|
|
387
|
+
return 0
|
|
388
|
+
fi
|
|
389
|
+
|
|
390
|
+
# MCPs replaced by curl subagents in v2.79.0
|
|
391
|
+
local deprecated_mcps=(
|
|
392
|
+
"hetzner-webapp"
|
|
393
|
+
"hetzner-brandlight"
|
|
394
|
+
"hetzner-marcusquinn"
|
|
395
|
+
"hetzner-storagebox"
|
|
396
|
+
"ahrefs"
|
|
397
|
+
"serper"
|
|
398
|
+
"dataforseo"
|
|
399
|
+
"hostinger-api"
|
|
400
|
+
"shadcn"
|
|
401
|
+
"repomix"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Tool rules to remove (for MCPs that no longer exist)
|
|
405
|
+
local deprecated_tools=(
|
|
406
|
+
"hetzner-*"
|
|
407
|
+
"hostinger-api_*"
|
|
408
|
+
"ahrefs_*"
|
|
409
|
+
"dataforseo_*"
|
|
410
|
+
"serper_*"
|
|
411
|
+
"shadcn_*"
|
|
412
|
+
"repomix_*"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
local cleaned=0
|
|
416
|
+
local tmp_config
|
|
417
|
+
tmp_config=$(mktemp)
|
|
418
|
+
trap 'rm -f "${tmp_config:-}"' RETURN
|
|
419
|
+
|
|
420
|
+
cp "$opencode_config" "$tmp_config"
|
|
421
|
+
|
|
422
|
+
for mcp in "${deprecated_mcps[@]}"; do
|
|
423
|
+
if jq -e ".mcp[\"$mcp\"]" "$tmp_config" >/dev/null 2>&1; then
|
|
424
|
+
jq "del(.mcp[\"$mcp\"])" "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
425
|
+
((++cleaned))
|
|
426
|
+
fi
|
|
427
|
+
done
|
|
428
|
+
|
|
429
|
+
for tool in "${deprecated_tools[@]}"; do
|
|
430
|
+
if jq -e ".tools[\"$tool\"]" "$tmp_config" >/dev/null 2>&1; then
|
|
431
|
+
jq "del(.tools[\"$tool\"])" "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
432
|
+
fi
|
|
433
|
+
done
|
|
434
|
+
|
|
435
|
+
# Also remove deprecated tool refs from SEO agent
|
|
436
|
+
if jq -e '.agent.SEO.tools["dataforseo_*"]' "$tmp_config" >/dev/null 2>&1; then
|
|
437
|
+
jq 'del(.agent.SEO.tools["dataforseo_*"]) | del(.agent.SEO.tools["serper_*"]) | del(.agent.SEO.tools["ahrefs_*"])' "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# Migrate npx/pipx commands to full binary paths (faster startup, PATH-independent)
|
|
441
|
+
# Parallel arrays avoid bash associative array issues with @ in package names
|
|
442
|
+
local -a mcp_pkgs=(
|
|
443
|
+
"chrome-devtools-mcp"
|
|
444
|
+
"mcp-server-gsc"
|
|
445
|
+
"playwriter"
|
|
446
|
+
"@steipete/macos-automator-mcp"
|
|
447
|
+
"@steipete/claude-code-mcp"
|
|
448
|
+
"analytics-mcp"
|
|
449
|
+
)
|
|
450
|
+
local -a mcp_bins=(
|
|
451
|
+
"chrome-devtools-mcp"
|
|
452
|
+
"mcp-server-gsc"
|
|
453
|
+
"playwriter"
|
|
454
|
+
"macos-automator-mcp"
|
|
455
|
+
"claude-code-mcp"
|
|
456
|
+
"analytics-mcp"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
local i
|
|
460
|
+
for i in "${!mcp_pkgs[@]}"; do
|
|
461
|
+
local pkg="${mcp_pkgs[$i]}"
|
|
462
|
+
local bin_name="${mcp_bins[$i]}"
|
|
463
|
+
# Find MCP key using npx/bunx/pipx for this package (single query)
|
|
464
|
+
local mcp_key
|
|
465
|
+
mcp_key=$(jq -r --arg pkg "$pkg" '.mcp | to_entries[] | select(.value.command != null) | select(.value.command | join(" ") | test("npx.*" + $pkg + "|bunx.*" + $pkg + "|pipx.*run.*" + $pkg)) | .key' "$tmp_config" 2>/dev/null | head -1)
|
|
466
|
+
|
|
467
|
+
if [[ -n "$mcp_key" ]]; then
|
|
468
|
+
# Resolve full path for the binary
|
|
469
|
+
local full_path
|
|
470
|
+
full_path=$(resolve_mcp_binary_path "$bin_name")
|
|
471
|
+
if [[ -n "$full_path" ]]; then
|
|
472
|
+
jq --arg k "$mcp_key" --arg p "$full_path" '.mcp[$k].command = [$p]' "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
473
|
+
((++cleaned))
|
|
474
|
+
fi
|
|
475
|
+
fi
|
|
476
|
+
done
|
|
477
|
+
|
|
478
|
+
# Migrate outscraper from bash -c wrapper to full binary path
|
|
479
|
+
if jq -e '.mcp.outscraper.command | join(" ") | test("bash.*outscraper")' "$tmp_config" >/dev/null 2>&1; then
|
|
480
|
+
local outscraper_path
|
|
481
|
+
outscraper_path=$(resolve_mcp_binary_path "outscraper-mcp-server")
|
|
482
|
+
if [[ -n "$outscraper_path" ]]; then
|
|
483
|
+
# Source the API key and set it in environment
|
|
484
|
+
local outscraper_key=""
|
|
485
|
+
if [[ -f "$HOME/.config/aidevops/credentials.sh" ]]; then
|
|
486
|
+
# shellcheck source=/dev/null
|
|
487
|
+
outscraper_key=$(source "$HOME/.config/aidevops/credentials.sh" && echo "${OUTSCRAPER_API_KEY:-}")
|
|
488
|
+
fi
|
|
489
|
+
jq --arg p "$outscraper_path" --arg key "$outscraper_key" '.mcp.outscraper.command = [$p] | .mcp.outscraper.environment = {"OUTSCRAPER_API_KEY": $key}' "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
490
|
+
((++cleaned))
|
|
491
|
+
fi
|
|
492
|
+
fi
|
|
493
|
+
|
|
494
|
+
if [[ $cleaned -gt 0 ]]; then
|
|
495
|
+
create_backup_with_rotation "$opencode_config" "opencode"
|
|
496
|
+
mv "$tmp_config" "$opencode_config"
|
|
497
|
+
print_info "Updated $cleaned MCP entry/entries in opencode.json (using full binary paths)"
|
|
498
|
+
else
|
|
499
|
+
rm -f "$tmp_config"
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
# Always resolve bare binary names to full paths (fixes PATH-dependent startup)
|
|
503
|
+
update_mcp_paths_in_opencode
|
|
504
|
+
|
|
505
|
+
return 0
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
# Disable MCPs globally that should only be enabled on-demand via subagents
|
|
509
|
+
# This reduces session startup context by disabling rarely-used MCPs
|
|
510
|
+
# - playwriter: ~3K tokens - enable via @playwriter subagent
|
|
511
|
+
# - augment-context-engine: ~1K tokens - enable via @augment-context-engine subagent
|
|
512
|
+
# - gh_grep: ~600 tokens - replaced by @github-search subagent (uses rg/bash)
|
|
513
|
+
# - google-analytics-mcp: ~800 tokens - enable via @google-analytics subagent
|
|
514
|
+
# - context7: ~800 tokens - enable via @context7 subagent (for library docs lookup)
|
|
515
|
+
disable_ondemand_mcps() {
|
|
516
|
+
local opencode_config
|
|
517
|
+
opencode_config=$(find_opencode_config) || return 0
|
|
518
|
+
|
|
519
|
+
if [[ ! -f "$opencode_config" ]]; then
|
|
520
|
+
return 0
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
if ! command -v jq &>/dev/null; then
|
|
524
|
+
return 0
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
# MCPs to disable globally (these have subagent alternatives or are unused)
|
|
528
|
+
# Note: use exact MCP key names from opencode.json
|
|
529
|
+
local -a ondemand_mcps=(
|
|
530
|
+
"playwriter"
|
|
531
|
+
"augment-context-engine"
|
|
532
|
+
"gh_grep"
|
|
533
|
+
"google-analytics-mcp"
|
|
534
|
+
"grep_app"
|
|
535
|
+
"websearch"
|
|
536
|
+
# KEEP ENABLED: context7 (library docs)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
local disabled=0
|
|
540
|
+
local changed=0
|
|
541
|
+
local tmp_config
|
|
542
|
+
tmp_config=$(mktemp)
|
|
543
|
+
trap 'rm -f "${tmp_config:-}"' RETURN
|
|
544
|
+
|
|
545
|
+
cp "$opencode_config" "$tmp_config"
|
|
546
|
+
|
|
547
|
+
for mcp in "${ondemand_mcps[@]}"; do
|
|
548
|
+
# Only disable MCPs that exist in the config
|
|
549
|
+
# Don't add fake entries - they break OpenCode's config validation
|
|
550
|
+
if jq -e ".mcp[\"$mcp\"]" "$tmp_config" >/dev/null 2>&1; then
|
|
551
|
+
local current_enabled
|
|
552
|
+
current_enabled=$(jq -r ".mcp[\"$mcp\"].enabled // \"true\"" "$tmp_config")
|
|
553
|
+
if [[ "$current_enabled" != "false" ]]; then
|
|
554
|
+
jq ".mcp[\"$mcp\"].enabled = false" "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
555
|
+
((++disabled))
|
|
556
|
+
fi
|
|
557
|
+
fi
|
|
558
|
+
done
|
|
559
|
+
|
|
560
|
+
# Remove invalid MCP entries added by v2.100.16 bug
|
|
561
|
+
# These have type "stdio" (invalid - only "local" or "remote" are valid)
|
|
562
|
+
# or command ["echo", "disabled"] which breaks OpenCode
|
|
563
|
+
local invalid_mcps=("grep_app" "websearch" "context7" "augment-context-engine")
|
|
564
|
+
for mcp in "${invalid_mcps[@]}"; do
|
|
565
|
+
# Check for invalid type "stdio" or dummy command
|
|
566
|
+
if jq -e ".mcp[\"$mcp\"].type == \"stdio\" or .mcp[\"$mcp\"].command[0] == \"echo\"" "$tmp_config" >/dev/null 2>&1; then
|
|
567
|
+
jq "del(.mcp[\"$mcp\"])" "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
568
|
+
print_info "Removed invalid MCP entry: $mcp"
|
|
569
|
+
changed=1
|
|
570
|
+
fi
|
|
571
|
+
done
|
|
572
|
+
|
|
573
|
+
# Re-enable MCPs that were accidentally disabled (v2.100.16-17 bug)
|
|
574
|
+
local -a keep_enabled=("context7")
|
|
575
|
+
for mcp in "${keep_enabled[@]}"; do
|
|
576
|
+
if jq -e ".mcp[\"$mcp\"].enabled == false" "$tmp_config" >/dev/null 2>&1; then
|
|
577
|
+
jq ".mcp[\"$mcp\"].enabled = true" "$tmp_config" >"${tmp_config}.new" && mv "${tmp_config}.new" "$tmp_config"
|
|
578
|
+
print_info "Re-enabled $mcp MCP"
|
|
579
|
+
changed=1
|
|
580
|
+
fi
|
|
581
|
+
done
|
|
582
|
+
|
|
583
|
+
if [[ $disabled -gt 0 || $changed -gt 0 ]]; then
|
|
584
|
+
create_backup_with_rotation "$opencode_config" "opencode"
|
|
585
|
+
mv "$tmp_config" "$opencode_config"
|
|
586
|
+
if [[ $disabled -gt 0 ]]; then
|
|
587
|
+
print_info "Disabled $disabled MCP(s) globally (use subagents to enable on-demand)"
|
|
588
|
+
fi
|
|
589
|
+
else
|
|
590
|
+
rm -f "$tmp_config"
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
return 0
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# Validate and repair OpenCode config schema
|
|
597
|
+
# Fixes common issues from manual editing or AI-generated configs:
|
|
598
|
+
# - MCP entries missing "type": "local" field
|
|
599
|
+
# - tools entries as objects {} instead of booleans
|
|
600
|
+
# If invalid, backs up and regenerates using the generator script
|
|
601
|
+
validate_opencode_config() {
|
|
602
|
+
local opencode_config
|
|
603
|
+
opencode_config=$(find_opencode_config) || return 0
|
|
604
|
+
|
|
605
|
+
if [[ ! -f "$opencode_config" ]]; then
|
|
606
|
+
return 0
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
if ! command -v jq &>/dev/null; then
|
|
610
|
+
return 0
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
local needs_repair=false
|
|
614
|
+
local issues=""
|
|
615
|
+
|
|
616
|
+
# Check 0: Remove deprecated top-level keys that OpenCode no longer recognizes
|
|
617
|
+
# "compaction" was removed in OpenCode v1.1.x - causes "Unrecognized key" error
|
|
618
|
+
local deprecated_keys=("compaction")
|
|
619
|
+
for key in "${deprecated_keys[@]}"; do
|
|
620
|
+
if jq -e ".[\"$key\"]" "$opencode_config" >/dev/null 2>&1; then
|
|
621
|
+
local tmp_fix
|
|
622
|
+
tmp_fix=$(mktemp)
|
|
623
|
+
trap 'rm -f "${tmp_fix:-}"' RETURN
|
|
624
|
+
if jq "del(.[\"$key\"])" "$opencode_config" >"$tmp_fix" 2>/dev/null; then
|
|
625
|
+
create_backup_with_rotation "$opencode_config" "opencode"
|
|
626
|
+
mv "$tmp_fix" "$opencode_config"
|
|
627
|
+
print_info "Removed deprecated '$key' key from OpenCode config"
|
|
628
|
+
else
|
|
629
|
+
rm -f "$tmp_fix"
|
|
630
|
+
fi
|
|
631
|
+
fi
|
|
632
|
+
done
|
|
633
|
+
|
|
634
|
+
# Check 1: MCP entries must have "type" field (usually "local")
|
|
635
|
+
# Invalid: {"mcp": {"foo": {"command": "..."}}}
|
|
636
|
+
# Valid: {"mcp": {"foo": {"type": "local", "command": "..."}}}
|
|
637
|
+
local mcps_without_type
|
|
638
|
+
mcps_without_type=$(jq -r '.mcp // {} | to_entries[] | select(.value.type == null and .value.command != null) | .key' "$opencode_config" 2>/dev/null | head -5)
|
|
639
|
+
if [[ -n "$mcps_without_type" ]]; then
|
|
640
|
+
needs_repair=true
|
|
641
|
+
issues="${issues}\n - MCP entries missing 'type' field: $(echo "$mcps_without_type" | tr '\n' ', ' | sed 's/,$//')"
|
|
642
|
+
fi
|
|
643
|
+
|
|
644
|
+
# Check 2: tools entries must be booleans, not objects
|
|
645
|
+
# Invalid: {"tools": {"gh_grep": {}}}
|
|
646
|
+
# Valid: {"tools": {"gh_grep": true}}
|
|
647
|
+
local tools_as_objects
|
|
648
|
+
tools_as_objects=$(jq -r '.tools // {} | to_entries[] | select(.value | type == "object") | .key' "$opencode_config" 2>/dev/null | head -5)
|
|
649
|
+
if [[ -n "$tools_as_objects" ]]; then
|
|
650
|
+
needs_repair=true
|
|
651
|
+
issues="${issues}\n - tools entries as objects instead of booleans: $(echo "$tools_as_objects" | tr '\n' ', ' | sed 's/,$//')"
|
|
652
|
+
fi
|
|
653
|
+
|
|
654
|
+
# Check 3: Try to parse with opencode (if available) to catch other schema issues
|
|
655
|
+
if command -v opencode &>/dev/null; then
|
|
656
|
+
local validation_output
|
|
657
|
+
if ! validation_output=$(opencode --version 2>&1); then
|
|
658
|
+
# If opencode fails to start, config might be invalid
|
|
659
|
+
if [[ "$validation_output" == *"Configuration is invalid"* ]]; then
|
|
660
|
+
needs_repair=true
|
|
661
|
+
issues="${issues}\n - OpenCode reports invalid configuration"
|
|
662
|
+
fi
|
|
663
|
+
fi
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
if [[ "$needs_repair" == "true" ]]; then
|
|
667
|
+
print_warning "OpenCode config has schema issues:$issues"
|
|
668
|
+
|
|
669
|
+
# Backup the invalid config
|
|
670
|
+
create_backup_with_rotation "$opencode_config" "opencode"
|
|
671
|
+
print_info "Backed up invalid config"
|
|
672
|
+
|
|
673
|
+
# Remove the invalid config so generator creates fresh one
|
|
674
|
+
rm -f "$opencode_config"
|
|
675
|
+
|
|
676
|
+
# Regenerate using the generator script
|
|
677
|
+
local generator_script="$HOME/.aidevops/agents/scripts/generate-opencode-agents.sh"
|
|
678
|
+
if [[ -x "$generator_script" ]]; then
|
|
679
|
+
print_info "Regenerating OpenCode config with correct schema..."
|
|
680
|
+
if "$generator_script" >/dev/null 2>&1; then
|
|
681
|
+
print_success "OpenCode config regenerated successfully"
|
|
682
|
+
else
|
|
683
|
+
print_warning "Config regeneration failed - run manually: $generator_script"
|
|
684
|
+
fi
|
|
685
|
+
else
|
|
686
|
+
print_warning "Generator script not found - run setup.sh again after agents are deployed"
|
|
687
|
+
fi
|
|
688
|
+
fi
|
|
689
|
+
|
|
690
|
+
return 0
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
# Migrate mcp-env.sh to credentials.sh (v2.105.0)
|
|
694
|
+
# Renames the credential file and creates backward-compatible symlink
|
|
695
|
+
migrate_mcp_env_to_credentials() {
|
|
696
|
+
local config_dir="$HOME/.config/aidevops"
|
|
697
|
+
local old_file="$config_dir/mcp-env.sh"
|
|
698
|
+
local new_file="$config_dir/credentials.sh"
|
|
699
|
+
local migrated=0
|
|
700
|
+
|
|
701
|
+
# Migrate root-level mcp-env.sh -> credentials.sh
|
|
702
|
+
if [[ -f "$old_file" && ! -L "$old_file" ]]; then
|
|
703
|
+
if [[ ! -f "$new_file" ]]; then
|
|
704
|
+
mv "$old_file" "$new_file"
|
|
705
|
+
chmod 600 "$new_file"
|
|
706
|
+
((++migrated))
|
|
707
|
+
print_info "Renamed mcp-env.sh to credentials.sh"
|
|
708
|
+
fi
|
|
709
|
+
# Create backward-compatible symlink
|
|
710
|
+
if [[ ! -L "$old_file" ]]; then
|
|
711
|
+
ln -sf "credentials.sh" "$old_file"
|
|
712
|
+
print_info "Created symlink mcp-env.sh -> credentials.sh"
|
|
713
|
+
fi
|
|
714
|
+
fi
|
|
715
|
+
|
|
716
|
+
# Migrate tenant-level mcp-env.sh -> credentials.sh
|
|
717
|
+
local tenants_dir="$config_dir/tenants"
|
|
718
|
+
if [[ -d "$tenants_dir" ]]; then
|
|
719
|
+
for tenant_dir in "$tenants_dir"/*/; do
|
|
720
|
+
[[ -d "$tenant_dir" ]] || continue
|
|
721
|
+
local tenant_old="$tenant_dir/mcp-env.sh"
|
|
722
|
+
local tenant_new="$tenant_dir/credentials.sh"
|
|
723
|
+
if [[ -f "$tenant_old" && ! -L "$tenant_old" ]]; then
|
|
724
|
+
if [[ ! -f "$tenant_new" ]]; then
|
|
725
|
+
mv "$tenant_old" "$tenant_new"
|
|
726
|
+
chmod 600 "$tenant_new"
|
|
727
|
+
((++migrated))
|
|
728
|
+
fi
|
|
729
|
+
if [[ ! -L "$tenant_old" ]]; then
|
|
730
|
+
ln -sf "credentials.sh" "$tenant_old"
|
|
731
|
+
fi
|
|
732
|
+
fi
|
|
733
|
+
done
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
# Update shell rc files that source the old path
|
|
737
|
+
for rc_file in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do
|
|
738
|
+
if [[ -f "$rc_file" ]] && grep -q 'source.*mcp-env\.sh' "$rc_file" 2>/dev/null; then
|
|
739
|
+
# shellcheck disable=SC2016
|
|
740
|
+
sed -i '' 's|source.*\.config/aidevops/mcp-env\.sh|source "$HOME/.config/aidevops/credentials.sh"|g' "$rc_file" 2>/dev/null ||
|
|
741
|
+
sed -i 's|source.*\.config/aidevops/mcp-env\.sh|source "$HOME/.config/aidevops/credentials.sh"|g' "$rc_file" 2>/dev/null || true
|
|
742
|
+
((++migrated))
|
|
743
|
+
print_info "Updated $rc_file to source credentials.sh"
|
|
744
|
+
fi
|
|
745
|
+
done
|
|
746
|
+
|
|
747
|
+
if [[ $migrated -gt 0 ]]; then
|
|
748
|
+
print_success "Migrated $migrated mcp-env.sh -> credentials.sh reference(s)"
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
return 0
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
# Migrate old config-backups to new per-type backup structure
|
|
755
|
+
# This runs once to clean up the legacy backup directory
|
|
756
|
+
migrate_old_backups() {
|
|
757
|
+
local old_backup_dir="$HOME/.aidevops/config-backups"
|
|
758
|
+
|
|
759
|
+
# Skip if old directory doesn't exist
|
|
760
|
+
if [[ ! -d "$old_backup_dir" ]]; then
|
|
761
|
+
return 0
|
|
762
|
+
fi
|
|
763
|
+
|
|
764
|
+
# Count old backups
|
|
765
|
+
local old_count
|
|
766
|
+
old_count=$(find "$old_backup_dir" -maxdepth 1 -type d -name "20*" 2>/dev/null | wc -l | tr -d ' ')
|
|
767
|
+
|
|
768
|
+
if [[ $old_count -eq 0 ]]; then
|
|
769
|
+
# Empty directory, just remove it
|
|
770
|
+
rm -rf "$old_backup_dir"
|
|
771
|
+
return 0
|
|
772
|
+
fi
|
|
773
|
+
|
|
774
|
+
print_info "Migrating $old_count old backups to new structure..."
|
|
775
|
+
|
|
776
|
+
# Create new backup directories
|
|
777
|
+
mkdir -p "$HOME/.aidevops/agents-backups"
|
|
778
|
+
mkdir -p "$HOME/.aidevops/opencode-backups"
|
|
779
|
+
|
|
780
|
+
# Move the most recent backups (up to BACKUP_KEEP_COUNT) to new locations
|
|
781
|
+
# Old backups contained mixed content, so we'll just keep the newest ones as agents backups
|
|
782
|
+
local migrated=0
|
|
783
|
+
for backup in $(find "$old_backup_dir" -maxdepth 1 -type d -name "20*" 2>/dev/null | sort -r | head -n "$BACKUP_KEEP_COUNT"); do
|
|
784
|
+
local backup_name
|
|
785
|
+
backup_name=$(basename "$backup")
|
|
786
|
+
|
|
787
|
+
# Check if it contains agents folder (most common)
|
|
788
|
+
if [[ -d "$backup/agents" ]]; then
|
|
789
|
+
mv "$backup" "$HOME/.aidevops/agents-backups/$backup_name"
|
|
790
|
+
((++migrated))
|
|
791
|
+
# Check if it contains opencode.json
|
|
792
|
+
elif [[ -f "$backup/opencode.json" ]]; then
|
|
793
|
+
mv "$backup" "$HOME/.aidevops/opencode-backups/$backup_name"
|
|
794
|
+
((++migrated))
|
|
795
|
+
fi
|
|
796
|
+
done
|
|
797
|
+
|
|
798
|
+
# Remove remaining old backups and the old directory
|
|
799
|
+
rm -rf "$old_backup_dir"
|
|
800
|
+
|
|
801
|
+
if [[ $migrated -gt 0 ]]; then
|
|
802
|
+
print_success "Migrated $migrated recent backups, removed $((old_count - migrated)) old backups"
|
|
803
|
+
else
|
|
804
|
+
print_info "Cleaned up $old_count old backups"
|
|
805
|
+
fi
|
|
806
|
+
|
|
807
|
+
return 0
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
# Migrate loop state from .claude/ to .agents/loop-state/ in user projects
|
|
811
|
+
# Also migrates from legacy .agents/loop-state/ to .agents/loop-state/
|
|
812
|
+
# The migration is non-destructive: moves files, doesn't delete originals until confirmed
|
|
813
|
+
migrate_loop_state_directories() {
|
|
814
|
+
print_info "Checking for legacy loop state directories..."
|
|
815
|
+
|
|
816
|
+
local migrated=0
|
|
817
|
+
local git_dirs=()
|
|
818
|
+
|
|
819
|
+
# Find Git repositories in common locations
|
|
820
|
+
# Check ~/Git/ and current directory's parent
|
|
821
|
+
for search_dir in "$HOME/Git" "$(dirname "$(pwd)")"; do
|
|
822
|
+
if [[ -d "$search_dir" ]]; then
|
|
823
|
+
while IFS= read -r -d '' git_dir; do
|
|
824
|
+
git_dirs+=("$(dirname "$git_dir")")
|
|
825
|
+
done < <(find "$search_dir" -maxdepth 3 -type d -name ".git" -print0 2>/dev/null)
|
|
826
|
+
fi
|
|
827
|
+
done
|
|
828
|
+
|
|
829
|
+
for repo_dir in "${git_dirs[@]}"; do
|
|
830
|
+
local old_state_dir="$repo_dir/.claude"
|
|
831
|
+
local legacy_state_dir="$repo_dir/.agent/loop-state"
|
|
832
|
+
local new_state_dir="$repo_dir/.agents/loop-state"
|
|
833
|
+
|
|
834
|
+
# Migrate from .claude/ (oldest legacy path)
|
|
835
|
+
if [[ -d "$old_state_dir" ]]; then
|
|
836
|
+
local has_loop_state=false
|
|
837
|
+
if [[ -f "$old_state_dir/ralph-loop.local.state" ]] ||
|
|
838
|
+
[[ -f "$old_state_dir/loop-state.json" ]] ||
|
|
839
|
+
[[ -d "$old_state_dir/receipts" ]]; then
|
|
840
|
+
has_loop_state=true
|
|
841
|
+
fi
|
|
842
|
+
|
|
843
|
+
if [[ "$has_loop_state" == "true" ]]; then
|
|
844
|
+
print_info "Found legacy loop state in: $repo_dir/.claude/"
|
|
845
|
+
mkdir -p "$new_state_dir"
|
|
846
|
+
|
|
847
|
+
for file in ralph-loop.local.state loop-state.json re-anchor.md guardrails.md; do
|
|
848
|
+
if [[ -f "$old_state_dir/$file" ]]; then
|
|
849
|
+
mv "$old_state_dir/$file" "$new_state_dir/"
|
|
850
|
+
print_info " Moved $file"
|
|
851
|
+
fi
|
|
852
|
+
done
|
|
853
|
+
|
|
854
|
+
if [[ -d "$old_state_dir/receipts" ]]; then
|
|
855
|
+
mv "$old_state_dir/receipts" "$new_state_dir/"
|
|
856
|
+
print_info " Moved receipts/"
|
|
857
|
+
fi
|
|
858
|
+
|
|
859
|
+
local remaining
|
|
860
|
+
remaining=$(find "$old_state_dir" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')
|
|
861
|
+
|
|
862
|
+
if [[ "$remaining" -eq 0 ]]; then
|
|
863
|
+
rmdir "$old_state_dir" 2>/dev/null && print_info " Removed empty .claude/"
|
|
864
|
+
else
|
|
865
|
+
print_warning " .claude/ has other files, not removing"
|
|
866
|
+
fi
|
|
867
|
+
|
|
868
|
+
((++migrated))
|
|
869
|
+
fi
|
|
870
|
+
fi
|
|
871
|
+
|
|
872
|
+
# Migrate from .agents/loop-state/ (v2.51.0-v2.103.0 path) to .agents/loop-state/
|
|
873
|
+
if [[ -d "$legacy_state_dir" ]] && [[ "$legacy_state_dir" != "$new_state_dir" ]]; then
|
|
874
|
+
print_info "Found legacy loop state in: $repo_dir/.agent/loop-state/"
|
|
875
|
+
mkdir -p "$new_state_dir"
|
|
876
|
+
|
|
877
|
+
# Move all files from old to new
|
|
878
|
+
if [[ -n "$(ls -A "$legacy_state_dir" 2>/dev/null)" ]]; then
|
|
879
|
+
cp -R "$legacy_state_dir"/* "$new_state_dir/" 2>/dev/null || true
|
|
880
|
+
rm -rf "$legacy_state_dir"
|
|
881
|
+
print_info " Migrated .agents/loop-state/ -> .agents/loop-state/"
|
|
882
|
+
((++migrated))
|
|
883
|
+
fi
|
|
884
|
+
fi
|
|
885
|
+
|
|
886
|
+
# Update .gitignore if needed
|
|
887
|
+
local gitignore="$repo_dir/.gitignore"
|
|
888
|
+
if [[ -f "$gitignore" ]]; then
|
|
889
|
+
if ! grep -q "^\.agents/loop-state/" "$gitignore" 2>/dev/null; then
|
|
890
|
+
echo ".agents/loop-state/" >>"$gitignore"
|
|
891
|
+
print_info " Added .agents/loop-state/ to .gitignore"
|
|
892
|
+
fi
|
|
893
|
+
fi
|
|
894
|
+
done
|
|
895
|
+
|
|
896
|
+
if [[ $migrated -gt 0 ]]; then
|
|
897
|
+
print_success "Migrated loop state in $migrated repositories"
|
|
898
|
+
else
|
|
899
|
+
print_info "No legacy loop state directories found"
|
|
900
|
+
fi
|
|
901
|
+
|
|
902
|
+
return 0
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
# Migrate pulse-repos.json into repos.json
|
|
906
|
+
# pulse-repos.json had slug/path/priority for supervisor-managed repos.
|
|
907
|
+
# Now repos.json is the single source of truth with slug, pulse, and priority fields.
|
|
908
|
+
migrate_pulse_repos_to_repos_json() {
|
|
909
|
+
local pulse_file="$HOME/.config/aidevops/pulse-repos.json"
|
|
910
|
+
local repos_file="$HOME/.config/aidevops/repos.json"
|
|
911
|
+
|
|
912
|
+
if [[ ! -f "$pulse_file" ]]; then
|
|
913
|
+
return 0
|
|
914
|
+
fi
|
|
915
|
+
|
|
916
|
+
if ! command -v jq &>/dev/null; then
|
|
917
|
+
print_warning "jq not installed — skipping pulse-repos.json migration"
|
|
918
|
+
return 0
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
if [[ ! -f "$repos_file" ]]; then
|
|
922
|
+
print_warning "repos.json not found — skipping pulse-repos.json migration"
|
|
923
|
+
return 0
|
|
924
|
+
fi
|
|
925
|
+
|
|
926
|
+
local migrated=0
|
|
927
|
+
local slug path priority
|
|
928
|
+
|
|
929
|
+
# Read each entry from pulse-repos.json and merge into repos.json
|
|
930
|
+
while IFS=$'\t' read -r slug path priority; do
|
|
931
|
+
[[ -z "$slug" ]] && continue
|
|
932
|
+
# Expand ~ in path
|
|
933
|
+
local expanded_path="${path/#\~/$HOME}"
|
|
934
|
+
|
|
935
|
+
# Check if this repo exists in repos.json by path
|
|
936
|
+
if jq -e --arg path "$expanded_path" '.initialized_repos[] | select(.path == $path)' "$repos_file" &>/dev/null; then
|
|
937
|
+
# Update existing entry: add slug, pulse, priority
|
|
938
|
+
local temp_file="${repos_file}.tmp"
|
|
939
|
+
jq --arg path "$expanded_path" --arg slug "$slug" --arg priority "$priority" \
|
|
940
|
+
'(.initialized_repos[] | select(.path == $path)) |= . + {slug: $slug, pulse: true, priority: $priority}' \
|
|
941
|
+
"$repos_file" >"$temp_file" && mv "$temp_file" "$repos_file"
|
|
942
|
+
((++migrated))
|
|
943
|
+
else
|
|
944
|
+
# Add new entry from pulse-repos.json
|
|
945
|
+
local temp_file="${repos_file}.tmp"
|
|
946
|
+
jq --arg path "$expanded_path" --arg slug "$slug" --arg priority "$priority" \
|
|
947
|
+
'.initialized_repos += [{path: $path, slug: $slug, pulse: true, priority: $priority}]' \
|
|
948
|
+
"$repos_file" >"$temp_file" && mv "$temp_file" "$repos_file"
|
|
949
|
+
((++migrated))
|
|
950
|
+
fi
|
|
951
|
+
done < <(jq -r '(.repos? // .)[] | [.slug, .path, .priority] | @tsv' "$pulse_file" 2>/dev/null)
|
|
952
|
+
|
|
953
|
+
if [[ $migrated -gt 0 ]]; then
|
|
954
|
+
print_success "Migrated $migrated repo(s) from pulse-repos.json into repos.json"
|
|
955
|
+
# Rename old file so it's not read again, but keep as backup
|
|
956
|
+
mv "$pulse_file" "${pulse_file}.migrated"
|
|
957
|
+
print_info "Renamed pulse-repos.json to pulse-repos.json.migrated"
|
|
958
|
+
fi
|
|
959
|
+
|
|
960
|
+
return 0
|
|
961
|
+
}
|