aidevops 3.13.95 → 3.14.1
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 +0 -1
- package/VERSION +1 -1
- package/aidevops.sh +44 -26
- package/package.json +1 -1
- package/setup.sh +25 -21
- package/aidevops-init-lib.sh +0 -1411
- package/aidevops-repos-lib.sh +0 -700
- package/aidevops-skills-plugin-lib.sh +0 -697
- package/aidevops-status-lib.sh +0 -141
- package/aidevops-update-lib.sh +0 -512
- package/aidevops-upgrade-planning-lib.sh +0 -370
- package/setup-modules/agent-deploy.sh +0 -1035
- package/setup-modules/agent-runtime.sh +0 -287
- package/setup-modules/config.sh +0 -478
- package/setup-modules/core.sh +0 -736
- package/setup-modules/mcp-setup.sh +0 -947
- package/setup-modules/migrations.sh +0 -1688
- package/setup-modules/plugins.sh +0 -728
- package/setup-modules/post-setup.sh +0 -301
- package/setup-modules/schedulers-linux.sh +0 -386
- package/setup-modules/schedulers-platform.sh +0 -1072
- package/setup-modules/schedulers-pulse.sh +0 -978
- package/setup-modules/schedulers.sh +0 -565
- package/setup-modules/shell-env.sh +0 -1240
- package/setup-modules/tool-beads.sh +0 -324
- package/setup-modules/tool-install.sh +0 -2134
|
@@ -1,1035 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
|
|
4
|
-
# Agent deployment functions: deploy_aidevops_agents, deploy_ai_templates, inject_agents_reference
|
|
5
|
-
# Part of aidevops setup.sh modularization (t316.3)
|
|
6
|
-
# Split from original agent-deploy.sh (t1940): runtime conversion → agent-runtime.sh, beads/hooks → tool-beads.sh
|
|
7
|
-
|
|
8
|
-
# Shell safety baseline
|
|
9
|
-
set -Eeuo pipefail
|
|
10
|
-
IFS=$'\n\t'
|
|
11
|
-
|
|
12
|
-
#######################################
|
|
13
|
-
# Restart the pulse process if it's running, so it picks up newly deployed
|
|
14
|
-
# scripts. Bash processes source files at startup only — file changes on
|
|
15
|
-
# disk don't affect a running process. The pulse is long-lived (hours/days)
|
|
16
|
-
# so it would run stale code indefinitely without a restart.
|
|
17
|
-
#
|
|
18
|
-
# The pulse auto-restarts via launchd/cron, so killing it is safe. If no
|
|
19
|
-
# auto-restart mechanism exists, we start it manually.
|
|
20
|
-
#######################################
|
|
21
|
-
_restart_pulse_if_running() {
|
|
22
|
-
# Anchor the pattern so it only matches the running script, not editors or
|
|
23
|
-
# grep commands that happen to contain "pulse-wrapper.sh" in their argv.
|
|
24
|
-
local pattern="(^|/)pulse-wrapper\\.sh( |$)"
|
|
25
|
-
|
|
26
|
-
if ! pgrep -f "$pattern" >/dev/null; then
|
|
27
|
-
# Not running, nothing to do
|
|
28
|
-
return 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
local old_pid
|
|
32
|
-
old_pid=$(pgrep -f "$pattern" | tail -1)
|
|
33
|
-
if [[ -z "$old_pid" ]]; then
|
|
34
|
-
return 0
|
|
35
|
-
fi
|
|
36
|
-
print_info "Restarting pulse (PID $old_pid) to load updated scripts..."
|
|
37
|
-
pkill -f "$pattern" || true
|
|
38
|
-
|
|
39
|
-
# Wait for it to die
|
|
40
|
-
local wait_count=0
|
|
41
|
-
while pgrep -f "$pattern" >/dev/null && [[ "$wait_count" -lt 10 ]]; do
|
|
42
|
-
sleep 1
|
|
43
|
-
wait_count=$((wait_count + 1))
|
|
44
|
-
done
|
|
45
|
-
|
|
46
|
-
# Give launchd/cron a moment to restart it
|
|
47
|
-
sleep 5
|
|
48
|
-
|
|
49
|
-
# Check if it auto-restarted with a different PID (guards against false
|
|
50
|
-
# success if the old process failed to terminate).
|
|
51
|
-
if pgrep -f "$pattern" >/dev/null; then
|
|
52
|
-
local new_pid
|
|
53
|
-
new_pid=$(pgrep -f "$pattern" | tail -1)
|
|
54
|
-
if [[ -n "$new_pid" ]] && [[ "$new_pid" != "$old_pid" ]]; then
|
|
55
|
-
print_success "Pulse restarted (new PID $new_pid)"
|
|
56
|
-
return 0
|
|
57
|
-
fi
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# No auto-restart — start it manually. Prefer the canonical repo source so a
|
|
61
|
-
# background launch never depends on a path inside ~/.aidevops/agents/scripts/
|
|
62
|
-
# that may be in the middle of a future deploy swap (must-not-be-wiped-during-deploy).
|
|
63
|
-
local pulse_script="${INSTALL_DIR:-.}/.agents/scripts/pulse-wrapper.sh"
|
|
64
|
-
if [[ ! -x "$pulse_script" ]]; then
|
|
65
|
-
pulse_script="${HOME}/.aidevops/agents/scripts/pulse-wrapper.sh"
|
|
66
|
-
fi
|
|
67
|
-
if [[ -x "$pulse_script" ]]; then
|
|
68
|
-
nohup "$pulse_script" >>"${HOME}/.aidevops/logs/pulse-wrapper.log" 2>&1 &
|
|
69
|
-
print_success "Pulse started manually (PID $!)"
|
|
70
|
-
else
|
|
71
|
-
print_warning "Pulse not restarted — $pulse_script not found"
|
|
72
|
-
fi
|
|
73
|
-
return 0
|
|
74
|
-
}
|
|
75
|
-
# shellcheck disable=SC2154 # rc is assigned by $? in the trap string
|
|
76
|
-
trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
|
|
77
|
-
shopt -s inherit_errexit 2>/dev/null || true
|
|
78
|
-
|
|
79
|
-
# Shared reference line injected into all runtime agent configs
|
|
80
|
-
readonly _AIDEVOPS_REFERENCE_LINE='Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities.'
|
|
81
|
-
|
|
82
|
-
deploy_ai_templates() {
|
|
83
|
-
print_info "Deploying AI assistant templates..."
|
|
84
|
-
|
|
85
|
-
if [[ -f "templates/deploy-templates.sh" ]]; then
|
|
86
|
-
if bash templates/deploy-templates.sh; then
|
|
87
|
-
print_success "AI assistant templates deployed successfully"
|
|
88
|
-
else
|
|
89
|
-
print_warning "Template deployment encountered issues (non-critical)"
|
|
90
|
-
fi
|
|
91
|
-
else
|
|
92
|
-
print_warning "Template deployment script not found - skipping"
|
|
93
|
-
fi
|
|
94
|
-
return 0
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
extract_opencode_prompts() {
|
|
98
|
-
local extract_script="${INSTALL_DIR:-.}/.agents/scripts/extract-opencode-prompts.sh"
|
|
99
|
-
if [[ -f "$extract_script" ]]; then
|
|
100
|
-
if bash "$extract_script"; then
|
|
101
|
-
print_success "OpenCode prompts extracted"
|
|
102
|
-
else
|
|
103
|
-
print_warning "OpenCode prompt extraction encountered issues (non-critical)"
|
|
104
|
-
fi
|
|
105
|
-
fi
|
|
106
|
-
return 0
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
check_opencode_prompt_drift() {
|
|
110
|
-
local drift_script="${INSTALL_DIR:-.}/.agents/scripts/opencode-prompt-drift-check.sh"
|
|
111
|
-
if [[ -f "$drift_script" ]]; then
|
|
112
|
-
local output exit_code=0
|
|
113
|
-
# 2>/dev/null is intentional: --quiet mode suppresses expected output; all exit
|
|
114
|
-
# codes (0=in-sync, 1=drift, other=error) are handled explicitly below.
|
|
115
|
-
output=$(bash "$drift_script" --quiet 2>/dev/null) || exit_code=$?
|
|
116
|
-
if [[ "$exit_code" -eq 1 && "$output" == PROMPT_DRIFT* ]]; then
|
|
117
|
-
local local_hash upstream_hash
|
|
118
|
-
local_hash=$(echo "$output" | cut -d'|' -f2)
|
|
119
|
-
upstream_hash=$(echo "$output" | cut -d'|' -f3)
|
|
120
|
-
print_warning "OpenCode upstream prompt has changed (${local_hash} → ${upstream_hash})"
|
|
121
|
-
print_info " Review: https://github.com/anomalyco/opencode/compare/${local_hash}...${upstream_hash}"
|
|
122
|
-
print_info " Update .agents/prompts/build.txt if needed"
|
|
123
|
-
elif [[ "$exit_code" -eq 0 ]]; then
|
|
124
|
-
print_success "OpenCode prompt in sync with upstream"
|
|
125
|
-
else
|
|
126
|
-
print_warning "Could not check prompt drift (network issue or missing dependency)"
|
|
127
|
-
fi
|
|
128
|
-
fi
|
|
129
|
-
return 0
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
# _deploy_agents_copy source_dir target_dir [plugin_namespaces...]
|
|
133
|
-
# Copies agent files using rsync (preferred) or tar fallback.
|
|
134
|
-
# Returns 0 on success, 1 on failure.
|
|
135
|
-
_deploy_agents_copy() {
|
|
136
|
-
local source_dir="$1"
|
|
137
|
-
local target_dir="$2"
|
|
138
|
-
shift 2
|
|
139
|
-
|
|
140
|
-
local deploy_ok=false
|
|
141
|
-
if command -v rsync &>/dev/null; then
|
|
142
|
-
local -a rsync_excludes=("--exclude=loop-state/" "--exclude=custom/" "--exclude=draft/")
|
|
143
|
-
for pns in "$@"; do
|
|
144
|
-
rsync_excludes+=("--exclude=${pns}/")
|
|
145
|
-
done
|
|
146
|
-
local rsync_timeout="${AIDEVOPS_RSYNC_TIMEOUT:-120}"
|
|
147
|
-
[[ "$rsync_timeout" =~ ^[0-9]+$ && "$rsync_timeout" -gt 0 ]] || rsync_timeout=120
|
|
148
|
-
# GH#22086: bound rsync I/O stalls so setup.sh --non-interactive can
|
|
149
|
-
# unwind via its EXIT trap instead of leaving a long-running setup owner
|
|
150
|
-
# and stale setup-noninteractive.lock.d behind.
|
|
151
|
-
if rsync -a --timeout="$rsync_timeout" "${rsync_excludes[@]}" "$source_dir/" "$target_dir/"; then
|
|
152
|
-
deploy_ok=true
|
|
153
|
-
fi
|
|
154
|
-
else
|
|
155
|
-
# Fallback: use tar with exclusions to match rsync behavior
|
|
156
|
-
local -a tar_excludes=("--exclude=loop-state" "--exclude=custom" "--exclude=draft")
|
|
157
|
-
for pns in "$@"; do
|
|
158
|
-
tar_excludes+=("--exclude=$pns")
|
|
159
|
-
done
|
|
160
|
-
if (cd "$source_dir" && tar cf - "${tar_excludes[@]}" .) | (cd "$target_dir" && tar xf -); then
|
|
161
|
-
deploy_ok=true
|
|
162
|
-
fi
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
if [[ "$deploy_ok" == "true" ]]; then
|
|
166
|
-
return 0
|
|
167
|
-
fi
|
|
168
|
-
return 1
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
# _is_reserved_agent_namespace namespace
|
|
172
|
-
# Returns 0 when a plugin namespace collides with a core aidevops agents
|
|
173
|
-
# directory. Such namespaces must never be passed as rsync/tar excludes because
|
|
174
|
-
# excluding scripts/ from the canonical source can deploy an agents tree that
|
|
175
|
-
# passes the copy step but fails the post-swap scripts/ invariant.
|
|
176
|
-
_is_reserved_agent_namespace() {
|
|
177
|
-
local namespace="$1"
|
|
178
|
-
|
|
179
|
-
case "$namespace" in
|
|
180
|
-
AGENTS.md|VERSION|advisories|commands|configs|custom|draft|hooks|plugins|prompts|reference|scripts|services|tools|workflows)
|
|
181
|
-
return 0
|
|
182
|
-
;;
|
|
183
|
-
esac
|
|
184
|
-
|
|
185
|
-
return 1
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
# _inject_plan_reminder target_dir
|
|
189
|
-
# Injects the extracted OpenCode plan-reminder into Plan+ if the placeholder is present.
|
|
190
|
-
_inject_plan_reminder() {
|
|
191
|
-
local target_dir="$1"
|
|
192
|
-
local plan_reminder="$HOME/.aidevops/cache/opencode-prompts/plan-reminder.txt"
|
|
193
|
-
local plan_plus="$target_dir/plan-plus.md"
|
|
194
|
-
if [[ ! -f "$plan_reminder" || ! -f "$plan_plus" ]]; then
|
|
195
|
-
return 0
|
|
196
|
-
fi
|
|
197
|
-
if ! grep -q "OPENCODE-PLAN-REMINDER-INJECT" "$plan_plus"; then
|
|
198
|
-
return 0
|
|
199
|
-
fi
|
|
200
|
-
local tmp_file in_placeholder
|
|
201
|
-
tmp_file=$(mktemp)
|
|
202
|
-
trap 'rm -f "${tmp_file:-}"' RETURN
|
|
203
|
-
in_placeholder=false
|
|
204
|
-
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
205
|
-
if [[ "$line" == *"OPENCODE-PLAN-REMINDER-INJECT-START"* ]]; then
|
|
206
|
-
echo "$line" >>"$tmp_file"
|
|
207
|
-
cat "$plan_reminder" >>"$tmp_file"
|
|
208
|
-
in_placeholder=true
|
|
209
|
-
elif [[ "$line" == *"OPENCODE-PLAN-REMINDER-INJECT-END"* ]]; then
|
|
210
|
-
echo "$line" >>"$tmp_file"
|
|
211
|
-
in_placeholder=false
|
|
212
|
-
elif [[ "$in_placeholder" == false ]]; then
|
|
213
|
-
echo "$line" >>"$tmp_file"
|
|
214
|
-
fi
|
|
215
|
-
done <"$plan_plus"
|
|
216
|
-
mv "$tmp_file" "$plan_plus"
|
|
217
|
-
print_info "Injected OpenCode plan-reminder into Plan+"
|
|
218
|
-
return 0
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
# _resolve_model_tiers_in_frontmatter target_dir
|
|
222
|
-
# Resolves tier shorthands (sonnet, haiku, opus, etc.) in YAML frontmatter
|
|
223
|
-
# `model:` fields to fully-qualified provider/model IDs using model-routing-table.json.
|
|
224
|
-
# This enables runtimes like OpenCode that consume `model:` literally (GH#18043).
|
|
225
|
-
# Source .md files keep tier names; deployed files get FQIDs.
|
|
226
|
-
# Only processes files with YAML frontmatter (--- delimited) where `model:` contains
|
|
227
|
-
# a bare tier name (no `/`). Already-qualified IDs are left unchanged.
|
|
228
|
-
_resolve_model_tiers_in_frontmatter() {
|
|
229
|
-
local target_dir="$1"
|
|
230
|
-
|
|
231
|
-
# Locate routing tables: merge custom overrides with default
|
|
232
|
-
local default_table="$target_dir/configs/model-routing-table.json"
|
|
233
|
-
local custom_table="$target_dir/custom/configs/model-routing-table.json"
|
|
234
|
-
|
|
235
|
-
if [[ ! -f "$default_table" ]]; then
|
|
236
|
-
print_warning "model-routing-table.json not found — skipping frontmatter model resolution"
|
|
237
|
-
return 0
|
|
238
|
-
fi
|
|
239
|
-
|
|
240
|
-
# Requires jq for JSON parsing
|
|
241
|
-
if ! command -v jq &>/dev/null; then
|
|
242
|
-
print_warning "jq not available — skipping frontmatter model resolution"
|
|
243
|
-
return 0
|
|
244
|
-
fi
|
|
245
|
-
|
|
246
|
-
# Build a sed script file from the routing table(s) in ONE jq call.
|
|
247
|
-
# Custom table overrides specific tiers; default fills in the rest.
|
|
248
|
-
# Each line is a separate sed command for cross-platform compatibility
|
|
249
|
-
# (macOS sed doesn't support ; as command separator inside {}).
|
|
250
|
-
# Generates replacements for both plain and commented forms:
|
|
251
|
-
# model: sonnet → model: anthropic/claude-sonnet-4-6
|
|
252
|
-
# model: sonnet # ... → model: anthropic/claude-sonnet-4-6 # ...
|
|
253
|
-
local sed_file
|
|
254
|
-
# t2997: drop .sed — XXXXXX must be at end for BSD mktemp; sed -f reads
|
|
255
|
-
# script content regardless of extension.
|
|
256
|
-
sed_file=$(mktemp "${TMPDIR:-/tmp}/model-resolve-XXXXXX")
|
|
257
|
-
if [[ -f "$custom_table" ]]; then
|
|
258
|
-
# Merge: custom tiers override default tiers (jq * operator)
|
|
259
|
-
jq -r -s '
|
|
260
|
-
(.[0].tiers // {}) * (.[1].tiers // {}) |
|
|
261
|
-
to_entries[] |
|
|
262
|
-
"s|^model: \(.key)$|model: \(.value.models[0])|",
|
|
263
|
-
"s|^model: \(.key) #|model: \(.value.models[0]) #|"
|
|
264
|
-
' "$default_table" "$custom_table" >"$sed_file" 2>/dev/null
|
|
265
|
-
else
|
|
266
|
-
jq -r '
|
|
267
|
-
.tiers | to_entries[] |
|
|
268
|
-
"s|^model: \(.key)$|model: \(.value.models[0])|",
|
|
269
|
-
"s|^model: \(.key) #|model: \(.value.models[0]) #|"
|
|
270
|
-
' "$default_table" >"$sed_file" 2>/dev/null
|
|
271
|
-
fi
|
|
272
|
-
|
|
273
|
-
if [[ ! -s "$sed_file" ]]; then
|
|
274
|
-
rm -f "$sed_file"
|
|
275
|
-
print_warning "No tiers found in routing table — skipping frontmatter model resolution"
|
|
276
|
-
return 0
|
|
277
|
-
fi
|
|
278
|
-
|
|
279
|
-
# Build a grep pattern to find only files with bare tier names.
|
|
280
|
-
# This avoids scanning all 3000+ .md files — only ~60 need changes.
|
|
281
|
-
# Extract tier names from the sed file (each line has the tier name after "model: ")
|
|
282
|
-
local tier_names
|
|
283
|
-
if [[ -f "$custom_table" ]]; then
|
|
284
|
-
tier_names=$(jq -r -s '(.[0].tiers // {}) * (.[1].tiers // {}) | keys[]' "$default_table" "$custom_table" 2>/dev/null | paste -sd'|' -)
|
|
285
|
-
else
|
|
286
|
-
tier_names=$(jq -r '.tiers | keys[]' "$default_table" 2>/dev/null | paste -sd'|' -)
|
|
287
|
-
fi
|
|
288
|
-
if [[ -z "$tier_names" ]]; then
|
|
289
|
-
rm -f "$sed_file"
|
|
290
|
-
return 0
|
|
291
|
-
fi
|
|
292
|
-
|
|
293
|
-
# Find candidate files: have a model: line with a bare tier name (no /)
|
|
294
|
-
# grep -rl is fast — scans content without loading full files
|
|
295
|
-
# The || true prevents set -e from exiting when grep finds no matches
|
|
296
|
-
local md_file
|
|
297
|
-
{ grep -rlE "^model: ($tier_names)(\$| #)" "$target_dir" --include='*.md' 2>/dev/null || true; } | while IFS= read -r md_file; do
|
|
298
|
-
[[ -n "$md_file" ]] || continue
|
|
299
|
-
# Verify the match is in YAML frontmatter (first line is ---)
|
|
300
|
-
local first_line
|
|
301
|
-
first_line=$(head -1 "$md_file" 2>/dev/null) || continue
|
|
302
|
-
[[ "$first_line" == "---" ]] || continue
|
|
303
|
-
|
|
304
|
-
# Apply sed replacements from the script file (macOS sed -i '' vs GNU sed -i)
|
|
305
|
-
sed -i '' -f "$sed_file" "$md_file" 2>/dev/null ||
|
|
306
|
-
sed -i -f "$sed_file" "$md_file" 2>/dev/null || true
|
|
307
|
-
done
|
|
308
|
-
|
|
309
|
-
# Count remaining unresolved files
|
|
310
|
-
local remaining
|
|
311
|
-
remaining=$({ grep -rlE "^model: ($tier_names)(\$| #)" "$target_dir" --include='*.md' 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
312
|
-
if [[ "$remaining" -eq 0 ]]; then
|
|
313
|
-
print_success "Resolved model tiers to FQIDs in deployed agent files (via model-routing-table.json)"
|
|
314
|
-
else
|
|
315
|
-
print_warning "Some model tiers could not be resolved ($remaining files remaining)"
|
|
316
|
-
fi
|
|
317
|
-
|
|
318
|
-
rm -f "$sed_file"
|
|
319
|
-
return 0
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
# _set_script_permissions_and_report target_dir
|
|
323
|
-
# Sets execute permissions on all deployed scripts and reports deployed counts.
|
|
324
|
-
_set_script_permissions_and_report() {
|
|
325
|
-
local target_dir="$1"
|
|
326
|
-
|
|
327
|
-
chmod +x "$target_dir/scripts/"*.sh 2>/dev/null || true
|
|
328
|
-
find "$target_dir/scripts" -mindepth 2 -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
|
329
|
-
|
|
330
|
-
local agent_count script_count
|
|
331
|
-
agent_count=$(find "$target_dir" -name "*.md" -type f | wc -l | tr -d ' ')
|
|
332
|
-
script_count=$(find "$target_dir/scripts" -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
333
|
-
print_info "Deployed $agent_count agent files and $script_count scripts"
|
|
334
|
-
return 0
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
# _count_deployed_agent_files target_dir
|
|
338
|
-
# Prints the number of files in the deployed agents tree. Non-numeric output is
|
|
339
|
-
# normalised to 0 so deploy verification never compares an empty string.
|
|
340
|
-
_count_deployed_agent_files() {
|
|
341
|
-
local target_dir="$1"
|
|
342
|
-
local file_count="0"
|
|
343
|
-
if [[ -d "$target_dir" ]]; then
|
|
344
|
-
file_count=$(find "$target_dir" -type f 2>/dev/null | wc -l | tr -d '[:space:]')
|
|
345
|
-
fi
|
|
346
|
-
[[ "$file_count" =~ ^[0-9]+$ ]] || file_count=0
|
|
347
|
-
printf '%s\n' "$file_count"
|
|
348
|
-
return 0
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
# _restore_latest_agents_backup target_dir
|
|
352
|
-
# Restores the newest agents backup after a failed deploy verification. Backups
|
|
353
|
-
# are created by create_backup_with_rotation as ~/.aidevops/agents-backups/<ts>/agents.
|
|
354
|
-
_restore_latest_agents_backup() {
|
|
355
|
-
local target_dir="$1"
|
|
356
|
-
local backup_base="$HOME/.aidevops/agents-backups"
|
|
357
|
-
local latest_backup=""
|
|
358
|
-
local parent_dir=""
|
|
359
|
-
local restore_staging=""
|
|
360
|
-
local old_dir=""
|
|
361
|
-
|
|
362
|
-
if [[ ! -d "$backup_base" ]]; then
|
|
363
|
-
print_warning "No agents backup directory found for restore: $backup_base"
|
|
364
|
-
return 1
|
|
365
|
-
fi
|
|
366
|
-
|
|
367
|
-
latest_backup=$(find "$backup_base" -maxdepth 2 -type d -name agents 2>/dev/null | sort | tail -n 1)
|
|
368
|
-
if [[ -z "$latest_backup" || ! -d "$latest_backup" ]]; then
|
|
369
|
-
print_warning "No restorable agents backup found under $backup_base"
|
|
370
|
-
return 1
|
|
371
|
-
fi
|
|
372
|
-
|
|
373
|
-
print_warning "Restoring agents from latest backup: $latest_backup"
|
|
374
|
-
parent_dir=$(dirname "$target_dir")
|
|
375
|
-
mkdir -p "$parent_dir"
|
|
376
|
-
restore_staging=$(mktemp -d "${target_dir}.restore.XXXXXX") || {
|
|
377
|
-
print_error "Failed to create agents restore staging directory"
|
|
378
|
-
return 1
|
|
379
|
-
}
|
|
380
|
-
old_dir="${target_dir}.restore-old.$$"
|
|
381
|
-
rm -rf "$old_dir"
|
|
382
|
-
|
|
383
|
-
if ! cp -a "$latest_backup/." "$restore_staging/"; then
|
|
384
|
-
print_error "Failed to stage agents backup for restore: $latest_backup"
|
|
385
|
-
rm -rf "$restore_staging"
|
|
386
|
-
return 1
|
|
387
|
-
fi
|
|
388
|
-
|
|
389
|
-
if [[ -d "$target_dir" ]]; then
|
|
390
|
-
if ! mv "$target_dir" "$old_dir"; then
|
|
391
|
-
print_error "Failed to move current agents directory aside during restore — agents directory preserved"
|
|
392
|
-
rm -rf "$restore_staging"
|
|
393
|
-
return 1
|
|
394
|
-
fi
|
|
395
|
-
fi
|
|
396
|
-
|
|
397
|
-
if mv "$restore_staging" "$target_dir"; then
|
|
398
|
-
rm -rf "$old_dir"
|
|
399
|
-
print_success "Restored agents directory from backup"
|
|
400
|
-
return 0
|
|
401
|
-
fi
|
|
402
|
-
|
|
403
|
-
print_error "Failed to move staged agents backup into place — attempting restore rollback"
|
|
404
|
-
if [[ -d "$old_dir" ]]; then
|
|
405
|
-
if mv "$old_dir" "$target_dir"; then
|
|
406
|
-
print_info "Restore rollback successful — previous agents directory restored"
|
|
407
|
-
else
|
|
408
|
-
print_error "Restore rollback failed — previous agents directory preserved in $old_dir"
|
|
409
|
-
fi
|
|
410
|
-
fi
|
|
411
|
-
rm -rf "$restore_staging"
|
|
412
|
-
return 1
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
# _verify_deployed_agents_tree target_dir
|
|
416
|
-
# Verifies the deployed agents tree is plausibly complete before .deployed-sha is
|
|
417
|
-
# written. This catches empty/partial deploys that would otherwise suppress the
|
|
418
|
-
# next auto-update retry by stamping the new SHA.
|
|
419
|
-
_verify_deployed_agents_tree() {
|
|
420
|
-
local target_dir="$1"
|
|
421
|
-
local min_files="${AIDEVOPS_AGENT_DEPLOY_MIN_FILES:-100}"
|
|
422
|
-
local file_count="0"
|
|
423
|
-
|
|
424
|
-
[[ "$min_files" =~ ^[0-9]+$ ]] || min_files=100
|
|
425
|
-
|
|
426
|
-
if [[ ! -d "$target_dir/scripts" ]]; then
|
|
427
|
-
print_error "Deploy verification failed: $target_dir/scripts missing after swap"
|
|
428
|
-
return 1
|
|
429
|
-
fi
|
|
430
|
-
|
|
431
|
-
file_count=$(_count_deployed_agent_files "$target_dir")
|
|
432
|
-
if [[ "$file_count" -lt "$min_files" ]]; then
|
|
433
|
-
print_error "Deploy verification failed: $target_dir has $file_count files (< $min_files)"
|
|
434
|
-
return 1
|
|
435
|
-
fi
|
|
436
|
-
|
|
437
|
-
return 0
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
# _install_opencode_plugin_deps target_dir
|
|
441
|
-
# Installs node_modules for the opencode-aidevops plugin.
|
|
442
|
-
# GH#17829: @bufbuild/protobuf was missing; GH#17891: only symlink on first run.
|
|
443
|
-
# Uses --omit=peer to skip the 630MB opencode-ai peer dep (the host app).
|
|
444
|
-
_install_opencode_plugin_deps() {
|
|
445
|
-
local target_dir="$1"
|
|
446
|
-
local oc_node_modules="$HOME/.config/opencode/node_modules"
|
|
447
|
-
local plugin_dir="$target_dir/plugins/opencode-aidevops"
|
|
448
|
-
|
|
449
|
-
if [[ ! -d "$plugin_dir" ]]; then
|
|
450
|
-
return 0
|
|
451
|
-
fi
|
|
452
|
-
|
|
453
|
-
# Only symlink if node_modules doesn't exist at all (first run)
|
|
454
|
-
if [[ ! -e "$plugin_dir/node_modules" ]]; then
|
|
455
|
-
if [[ -d "$oc_node_modules" ]]; then
|
|
456
|
-
ln -sf "$oc_node_modules" "$plugin_dir/node_modules" 2>/dev/null || true
|
|
457
|
-
fi
|
|
458
|
-
fi
|
|
459
|
-
|
|
460
|
-
# Verify critical dependency is available; npm install if not
|
|
461
|
-
if [[ ! -d "$plugin_dir/node_modules/@bufbuild/protobuf" ]]; then
|
|
462
|
-
if command -v npm &>/dev/null; then
|
|
463
|
-
# Remove symlink if present so npm creates a local node_modules
|
|
464
|
-
[[ -L "$plugin_dir/node_modules" ]] && rm "$plugin_dir/node_modules"
|
|
465
|
-
npm install --omit=dev --omit=peer --prefix "$plugin_dir" >/dev/null 2>&1 ||
|
|
466
|
-
print_warning "Failed to install plugin dependencies (non-blocking)"
|
|
467
|
-
fi
|
|
468
|
-
fi
|
|
469
|
-
return 0
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
# _atomic_stage_and_deploy_agents source_dir target_dir [plugin_namespaces...]
|
|
473
|
-
# Stages a copy in a per-process target_dir.staging.* directory, carries over
|
|
474
|
-
# preserved dirs (custom/, draft/, and any plugin namespaces), then atomically
|
|
475
|
-
# swaps staging into place.
|
|
476
|
-
# Returns 0 on success, 1 on failure.
|
|
477
|
-
_atomic_stage_and_deploy_agents() {
|
|
478
|
-
local source_dir="$1"
|
|
479
|
-
local target_dir="$2"
|
|
480
|
-
shift 2
|
|
481
|
-
local -a plugin_namespaces=("$@")
|
|
482
|
-
|
|
483
|
-
# Atomic deploy: build a staging directory, then swap it into place.
|
|
484
|
-
# Previously, clean + copy happened in-place, creating a window where
|
|
485
|
-
# scripts were missing. The pulse could dispatch workers mid-deploy,
|
|
486
|
-
# hitting "No such file or directory" errors. Now we:
|
|
487
|
-
# 1. rsync into a unique staging dir (target_dir.staging.*)
|
|
488
|
-
# 2. Move preserved dirs (custom/, draft/, plugins) from live to staging
|
|
489
|
-
# 3. mv live → .old.*, mv staging → live (atomic on same filesystem)
|
|
490
|
-
# 4. rm .old.*
|
|
491
|
-
#
|
|
492
|
-
# GH#22063: the staging/backup paths must be unique per setup process. Fixed
|
|
493
|
-
# names such as target_dir.staging let a concurrent setup cleanup remove the
|
|
494
|
-
# directory while rsync is still writing into it, producing renameat/move_file
|
|
495
|
-
# ENOENT failures even though the canonical .agents/ source is valid.
|
|
496
|
-
local staging_dir old_dir
|
|
497
|
-
staging_dir=$(mktemp -d "${target_dir}.staging.XXXXXX") || {
|
|
498
|
-
print_error "Failed to create agents staging directory"
|
|
499
|
-
return 1
|
|
500
|
-
}
|
|
501
|
-
old_dir="${target_dir}.old.$$"
|
|
502
|
-
rm -rf "$old_dir"
|
|
503
|
-
|
|
504
|
-
# Copy source into staging
|
|
505
|
-
local copy_rc
|
|
506
|
-
if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
|
|
507
|
-
_deploy_agents_copy "$source_dir" "$staging_dir" "${plugin_namespaces[@]}"
|
|
508
|
-
copy_rc=$?
|
|
509
|
-
else
|
|
510
|
-
_deploy_agents_copy "$source_dir" "$staging_dir"
|
|
511
|
-
copy_rc=$?
|
|
512
|
-
fi
|
|
513
|
-
if [[ "$copy_rc" -ne 0 ]]; then
|
|
514
|
-
print_error "Failed to deploy agents to staging directory"
|
|
515
|
-
rm -rf "$staging_dir"
|
|
516
|
-
return 1
|
|
517
|
-
fi
|
|
518
|
-
|
|
519
|
-
# Carry over preserved directories from live target to staging
|
|
520
|
-
local -a preserved_dirs=("custom" "draft")
|
|
521
|
-
if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
|
|
522
|
-
for pns in "${plugin_namespaces[@]}"; do
|
|
523
|
-
preserved_dirs+=("$pns")
|
|
524
|
-
done
|
|
525
|
-
fi
|
|
526
|
-
for pdir in "${preserved_dirs[@]}"; do
|
|
527
|
-
if [[ -d "$target_dir/$pdir" ]]; then
|
|
528
|
-
# Copy user dirs into staging so they survive the swap
|
|
529
|
-
cp -a "$target_dir/$pdir" "$staging_dir/$pdir" 2>/dev/null || true
|
|
530
|
-
fi
|
|
531
|
-
done
|
|
532
|
-
|
|
533
|
-
# Atomic swap: mv is atomic on the same filesystem (POSIX rename()).
|
|
534
|
-
# IMPORTANT: explicit error checks are REQUIRED here because this function
|
|
535
|
-
# is called via `|| return 1` which disables set -e inside the function
|
|
536
|
-
# body (bash set -e semantics: disabled in any function called as part of
|
|
537
|
-
# a compound list such as `fn || ...`). Without these checks, a failed mv
|
|
538
|
-
# falls through silently, the backup is deleted, and the function returns
|
|
539
|
-
# 0 with $target_dir absent — the root cause of GH#22014 where worktree
|
|
540
|
-
# setup left ~/.aidevops/agents missing while reporting [SETUP_COMPLETE].
|
|
541
|
-
if [[ -d "$target_dir" ]]; then
|
|
542
|
-
if ! mv "$target_dir" "$old_dir"; then
|
|
543
|
-
print_error "Failed to move live agents to backup ($old_dir) — agents directory preserved"
|
|
544
|
-
rm -rf "$staging_dir"
|
|
545
|
-
return 1
|
|
546
|
-
fi
|
|
547
|
-
fi
|
|
548
|
-
if ! mv "$staging_dir" "$target_dir"; then
|
|
549
|
-
print_error "Failed to move staging to live agents directory — attempting rollback"
|
|
550
|
-
# Restore the previous agents dir from backup so the system stays functional.
|
|
551
|
-
if [[ -d "$old_dir" ]]; then
|
|
552
|
-
if mv "$old_dir" "$target_dir"; then
|
|
553
|
-
print_info "Rollback successful — previous agents directory restored"
|
|
554
|
-
else
|
|
555
|
-
print_error "Rollback failed — agents directory is missing! Previous state preserved in $old_dir"
|
|
556
|
-
fi
|
|
557
|
-
fi
|
|
558
|
-
rm -rf "$staging_dir"
|
|
559
|
-
return 1
|
|
560
|
-
fi
|
|
561
|
-
rm -rf "$old_dir"
|
|
562
|
-
return 0
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
# _deploy_version_file target_dir repo_dir
|
|
566
|
-
# Copies VERSION file from repo root to the deployed agents directory.
|
|
567
|
-
_deploy_version_file() {
|
|
568
|
-
local target_dir="$1"
|
|
569
|
-
local repo_dir="$2"
|
|
570
|
-
|
|
571
|
-
if [[ -f "$repo_dir/VERSION" ]]; then
|
|
572
|
-
if cp "$repo_dir/VERSION" "$target_dir/VERSION"; then
|
|
573
|
-
print_info "Copied VERSION file to deployed agents"
|
|
574
|
-
else
|
|
575
|
-
print_warning "Failed to copy VERSION file (Plan+ may not read version correctly)"
|
|
576
|
-
fi
|
|
577
|
-
else
|
|
578
|
-
print_warning "VERSION file not found in repo root"
|
|
579
|
-
fi
|
|
580
|
-
return 0
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
# _deploy_security_advisories_files source_dir
|
|
584
|
-
# Copies *.advisory files to ~/.aidevops/advisories/ (shown in session greeting).
|
|
585
|
-
_deploy_security_advisories_files() {
|
|
586
|
-
local source_dir="$1"
|
|
587
|
-
local advisories_source="$source_dir/advisories"
|
|
588
|
-
local advisories_target="$HOME/.aidevops/advisories"
|
|
589
|
-
|
|
590
|
-
if [[ ! -d "$advisories_source" ]]; then
|
|
591
|
-
return 0
|
|
592
|
-
fi
|
|
593
|
-
mkdir -p "$advisories_target"
|
|
594
|
-
local adv_count=0
|
|
595
|
-
for adv_file in "$advisories_source"/*.advisory; do
|
|
596
|
-
[[ -f "$adv_file" ]] || continue
|
|
597
|
-
cp "$adv_file" "$advisories_target/"
|
|
598
|
-
adv_count=$((adv_count + 1))
|
|
599
|
-
done
|
|
600
|
-
if [[ "$adv_count" -gt 0 ]]; then
|
|
601
|
-
print_info "Deployed $adv_count security advisory/advisories"
|
|
602
|
-
fi
|
|
603
|
-
return 0
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
# _migrate_mailbox_if_needed target_dir
|
|
607
|
-
# Migrates mailbox from legacy TOON files to SQLite if old files exist.
|
|
608
|
-
_migrate_mailbox_if_needed() {
|
|
609
|
-
local target_dir="$1"
|
|
610
|
-
local aidevops_workspace_dir="${AIDEVOPS_WORKSPACE_DIR:-$HOME/.aidevops/.agent-workspace}"
|
|
611
|
-
local mail_dir="${AIDEVOPS_MAIL_DIR:-${aidevops_workspace_dir}/mail}"
|
|
612
|
-
local mail_script="$target_dir/scripts/mail-helper.sh"
|
|
613
|
-
|
|
614
|
-
if [[ -x "$mail_script" ]] && find "$mail_dir" -name "*.toon" 2>/dev/null | grep -q .; then
|
|
615
|
-
if "$mail_script" migrate; then
|
|
616
|
-
print_success "Mailbox migration complete"
|
|
617
|
-
else
|
|
618
|
-
print_warning "Mailbox migration had issues (non-critical, old files preserved)"
|
|
619
|
-
fi
|
|
620
|
-
fi
|
|
621
|
-
return 0
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
# _migrate_wavespeed_md target_dir
|
|
625
|
-
# Removes stale wavespeed.md from deprecated services/ai-generation/ path (v2.111+).
|
|
626
|
-
_migrate_wavespeed_md() {
|
|
627
|
-
local target_dir="$1"
|
|
628
|
-
local old_wavespeed="$target_dir/services/ai-generation/wavespeed.md"
|
|
629
|
-
|
|
630
|
-
if [[ -f "$old_wavespeed" ]]; then
|
|
631
|
-
rm -f "$old_wavespeed"
|
|
632
|
-
rmdir "$target_dir/services/ai-generation" 2>/dev/null || true
|
|
633
|
-
print_info "Migrated wavespeed.md from services/ai-generation/ to tools/video/"
|
|
634
|
-
fi
|
|
635
|
-
return 0
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
# _deploy_agents_post_copy target_dir repo_dir source_dir plugins_file
|
|
639
|
-
# Orchestrates all post-copy steps: permissions, VERSION, advisories, plan-reminder,
|
|
640
|
-
# mailbox migration, stale-file migration, model resolution, and plugin deployment.
|
|
641
|
-
_deploy_agents_post_copy() {
|
|
642
|
-
local target_dir="$1"
|
|
643
|
-
local repo_dir="$2"
|
|
644
|
-
local source_dir="$3"
|
|
645
|
-
local plugins_file="$4"
|
|
646
|
-
|
|
647
|
-
_set_script_permissions_and_report "$target_dir"
|
|
648
|
-
_install_opencode_plugin_deps "$target_dir"
|
|
649
|
-
_deploy_version_file "$target_dir" "$repo_dir"
|
|
650
|
-
_deploy_security_advisories_files "$source_dir"
|
|
651
|
-
_inject_plan_reminder "$target_dir"
|
|
652
|
-
_migrate_mailbox_if_needed "$target_dir"
|
|
653
|
-
_migrate_wavespeed_md "$target_dir"
|
|
654
|
-
# Source files keep tier names (sonnet, haiku, opus); deployed files get
|
|
655
|
-
# fully-qualified IDs (anthropic/claude-sonnet-4-6) that runtimes like
|
|
656
|
-
# OpenCode can consume directly (GH#18043).
|
|
657
|
-
_resolve_model_tiers_in_frontmatter "$target_dir"
|
|
658
|
-
deploy_plugins "$target_dir" "$plugins_file"
|
|
659
|
-
return 0
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
# _warn_deployed_script_drift source_dir target_dir
|
|
663
|
-
# Compares deployed scripts against canonical source and warns if any differ.
|
|
664
|
-
# This catches the case where someone edited ~/.aidevops/agents/scripts/ directly
|
|
665
|
-
# (those edits are overwritten by every deploy). Emits a warning listing drifted
|
|
666
|
-
# files and the canonical source path to edit instead.
|
|
667
|
-
# Non-fatal: always returns 0 so deployment proceeds.
|
|
668
|
-
#
|
|
669
|
-
# Performance: uses a single rsync --checksum --dry-run call instead of one
|
|
670
|
-
# diff -q subprocess per script (was 783 calls → now 1 call; t3221).
|
|
671
|
-
_warn_deployed_script_drift() {
|
|
672
|
-
local source_dir="$1"
|
|
673
|
-
local target_dir="$2"
|
|
674
|
-
local source_scripts="$source_dir/scripts"
|
|
675
|
-
local target_scripts="$target_dir/scripts"
|
|
676
|
-
|
|
677
|
-
if [[ ! -d "$source_scripts" || ! -d "$target_scripts" ]]; then
|
|
678
|
-
return 0
|
|
679
|
-
fi
|
|
680
|
-
|
|
681
|
-
local -a drifted=()
|
|
682
|
-
if command -v rsync &>/dev/null; then
|
|
683
|
-
# Single bulk comparison: rsync --checksum --dry-run reports changed files
|
|
684
|
-
# without transferring anything. --out-format='%f' prints only the relative
|
|
685
|
-
# path of each changed file. Filter to top-level *.sh only (no subdirs).
|
|
686
|
-
local changed_file
|
|
687
|
-
while IFS= read -r changed_file; do
|
|
688
|
-
[[ -n "$changed_file" ]] || continue
|
|
689
|
-
# Skip subdirectory scripts (only warn about top-level scripts/)
|
|
690
|
-
[[ "$changed_file" == */* ]] && continue
|
|
691
|
-
[[ "$changed_file" == *.sh ]] || continue
|
|
692
|
-
drifted+=("$changed_file")
|
|
693
|
-
done < <(rsync --checksum --dry-run \
|
|
694
|
-
--out-format='%f' \
|
|
695
|
-
--include='*.sh' --exclude='*/' --exclude='*' \
|
|
696
|
-
"$source_scripts/" "$target_scripts/" 2>/dev/null || true)
|
|
697
|
-
elif command -v diff &>/dev/null; then
|
|
698
|
-
# Fallback: one diff -q per script (slow, only reached when rsync absent)
|
|
699
|
-
local f bn
|
|
700
|
-
for f in "$target_scripts"/*.sh; do
|
|
701
|
-
[[ -f "$f" ]] || continue
|
|
702
|
-
bn=$(basename "$f")
|
|
703
|
-
local src="$source_scripts/$bn"
|
|
704
|
-
if [[ -f "$src" ]] && ! diff -q "$src" "$f" &>/dev/null; then
|
|
705
|
-
drifted+=("$bn")
|
|
706
|
-
fi
|
|
707
|
-
done
|
|
708
|
-
fi
|
|
709
|
-
|
|
710
|
-
if [[ ${#drifted[@]} -gt 0 ]]; then
|
|
711
|
-
print_warning "Deployed scripts differ from canonical source (local edits will be overwritten; backup will be created):"
|
|
712
|
-
for bn in "${drifted[@]}"; do
|
|
713
|
-
print_warning " $target_scripts/$bn"
|
|
714
|
-
print_warning " → canonical: $source_scripts/$bn"
|
|
715
|
-
done
|
|
716
|
-
print_warning "To keep personal scripts: use $target_dir/custom/scripts/"
|
|
717
|
-
print_warning "To fix the canonical source: edit $source_scripts/ and re-run setup.sh"
|
|
718
|
-
fi
|
|
719
|
-
return 0
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
deploy_aidevops_agents() {
|
|
723
|
-
print_info "Deploying aidevops agents to ~/.aidevops/agents/..."
|
|
724
|
-
|
|
725
|
-
# Use INSTALL_DIR (set by setup.sh) — BASH_SOURCE[0] points to setup-modules/
|
|
726
|
-
# which is not the repo root, so we can't derive .agents/ from it
|
|
727
|
-
local repo_dir="${INSTALL_DIR:?INSTALL_DIR must be set by setup.sh}"
|
|
728
|
-
local source_dir="$repo_dir/.agents"
|
|
729
|
-
local target_dir="$HOME/.aidevops/agents"
|
|
730
|
-
local plugins_file="$HOME/.config/aidevops/plugins.json"
|
|
731
|
-
|
|
732
|
-
# Validate source directory exists (catches curl install from wrong directory)
|
|
733
|
-
if [[ ! -d "$source_dir" ]]; then
|
|
734
|
-
print_error "Agent source directory not found: $source_dir"
|
|
735
|
-
print_info "This usually means setup.sh was run from the wrong directory."
|
|
736
|
-
print_info "The bootstrap should have cloned the repo and re-executed."
|
|
737
|
-
print_info ""
|
|
738
|
-
print_info "To fix manually:"
|
|
739
|
-
print_info " cd ~/Git/aidevops && ./setup.sh"
|
|
740
|
-
return 1
|
|
741
|
-
fi
|
|
742
|
-
|
|
743
|
-
# Collect plugin namespace directories to preserve during deployment
|
|
744
|
-
local -a plugin_namespaces=()
|
|
745
|
-
if [[ -f "$plugins_file" ]] && command -v jq &>/dev/null; then
|
|
746
|
-
local ns safe_ns
|
|
747
|
-
while IFS= read -r ns; do
|
|
748
|
-
if [[ -n "$ns" ]] && safe_ns=$(sanitize_plugin_namespace "$ns" 2>/dev/null); then
|
|
749
|
-
if _is_reserved_agent_namespace "$safe_ns"; then
|
|
750
|
-
print_warning "Skipping plugin namespace that collides with core agents directory: $safe_ns"
|
|
751
|
-
continue
|
|
752
|
-
fi
|
|
753
|
-
plugin_namespaces+=("$safe_ns")
|
|
754
|
-
fi
|
|
755
|
-
done < <(jq -r '.plugins[].namespace // empty' "$plugins_file" 2>/dev/null)
|
|
756
|
-
fi
|
|
757
|
-
|
|
758
|
-
# Warn if deployed scripts have been locally modified (GH#17414).
|
|
759
|
-
# These edits will be overwritten — users must edit the canonical source.
|
|
760
|
-
if [[ -d "$target_dir" ]]; then
|
|
761
|
-
_warn_deployed_script_drift "$source_dir" "$target_dir"
|
|
762
|
-
fi
|
|
763
|
-
|
|
764
|
-
# Create backup if target exists (with rotation).
|
|
765
|
-
# Skip when the deployed SHA matches the current HEAD — nothing changed on
|
|
766
|
-
# disk, so there is nothing worth backing up (t3221: steady-state perf).
|
|
767
|
-
if [[ -d "$target_dir" ]]; then
|
|
768
|
-
local _cur_sha _dep_sha
|
|
769
|
-
_cur_sha=$(git -C "$repo_dir" rev-parse HEAD 2>/dev/null || echo "")
|
|
770
|
-
_dep_sha=$(cat "${HOME}/.aidevops/.deployed-sha" 2>/dev/null || echo "")
|
|
771
|
-
if [[ -n "$_cur_sha" && -n "$_dep_sha" && "$_cur_sha" == "$_dep_sha" ]]; then
|
|
772
|
-
print_info "No changes since last deploy (${_cur_sha:0:8}) — skipping backup"
|
|
773
|
-
else
|
|
774
|
-
create_backup_with_rotation "$target_dir" "agents"
|
|
775
|
-
fi
|
|
776
|
-
fi
|
|
777
|
-
|
|
778
|
-
mkdir -p "$target_dir"
|
|
779
|
-
|
|
780
|
-
# Atomically copy source to staging, carry over user dirs, then swap.
|
|
781
|
-
if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
|
|
782
|
-
_atomic_stage_and_deploy_agents "$source_dir" "$target_dir" "${plugin_namespaces[@]}" || return 1
|
|
783
|
-
else
|
|
784
|
-
_atomic_stage_and_deploy_agents "$source_dir" "$target_dir" || return 1
|
|
785
|
-
fi
|
|
786
|
-
|
|
787
|
-
# Postcondition: verify the swap actually produced a functional agents dir.
|
|
788
|
-
# _atomic_stage_and_deploy_agents returns 0 on success, but this belt-and-
|
|
789
|
-
# suspenders check catches future regressions where the function returns early
|
|
790
|
-
# without correctly populating $target_dir (GH#22014/GH#21973). Do not write
|
|
791
|
-
# .deployed-sha unless this passes; otherwise auto-update would suppress the
|
|
792
|
-
# next retry even though agents/ is empty or partial.
|
|
793
|
-
if ! _verify_deployed_agents_tree "$target_dir"; then
|
|
794
|
-
print_error "The agents directory was not correctly deployed — setup cannot continue"
|
|
795
|
-
_restore_latest_agents_backup "$target_dir" || true
|
|
796
|
-
return 1
|
|
797
|
-
fi
|
|
798
|
-
|
|
799
|
-
print_success "Deployed agents to $target_dir"
|
|
800
|
-
_deploy_agents_post_copy "$target_dir" "$repo_dir" "$source_dir" "$plugins_file"
|
|
801
|
-
|
|
802
|
-
# Write deployed-SHA stamp BEFORE the pulse restart so the stamp is
|
|
803
|
-
# available immediately for subsequent setup steps and the next run's
|
|
804
|
-
# backup-skip check (t3221). Previously written after the blocking
|
|
805
|
-
# restart wait; moving it here has no correctness impact — the deploy
|
|
806
|
-
# is already fully on disk at this point.
|
|
807
|
-
# t2156: enables auto-redeploy when local commits land between releases.
|
|
808
|
-
local deployed_sha
|
|
809
|
-
deployed_sha=$(git -C "$repo_dir" rev-parse HEAD 2>/dev/null || echo "")
|
|
810
|
-
if [[ -n "$deployed_sha" ]]; then
|
|
811
|
-
local aidevops_dir="${HOME}/.aidevops"
|
|
812
|
-
mkdir -p "$aidevops_dir"
|
|
813
|
-
printf '%s\n' "$deployed_sha" >"${aidevops_dir}/.deployed-sha"
|
|
814
|
-
fi
|
|
815
|
-
|
|
816
|
-
# Restart pulse in the background — bash processes load source files at
|
|
817
|
-
# startup and don't re-read them when files change on disk. Without a
|
|
818
|
-
# restart, fixes to pulse-*.sh, dispatch-dedup-*.sh, and other sourced
|
|
819
|
-
# scripts don't take effect until the next manual restart.
|
|
820
|
-
#
|
|
821
|
-
# t3221: running this asynchronously saves the 10-15s blocking wait
|
|
822
|
-
# (pkill + up to 10s die-wait + sleep 5 launchd grace period). The
|
|
823
|
-
# deploy is already complete on disk; the pulse picks up the new scripts
|
|
824
|
-
# once it restarts regardless of when that happens relative to setup.sh
|
|
825
|
-
# finishing. disown prevents SIGHUP propagation if setup.sh is sourced
|
|
826
|
-
# interactively; in script mode the orphan survives the exit anyway.
|
|
827
|
-
_restart_pulse_if_running &
|
|
828
|
-
disown
|
|
829
|
-
|
|
830
|
-
return 0
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
inject_agents_reference() {
|
|
834
|
-
print_info "Adding aidevops reference to AI assistant configurations..."
|
|
835
|
-
|
|
836
|
-
# Delegate to prompt-injection-adapter.sh (t1665.3) which handles all runtimes.
|
|
837
|
-
# The adapter deploys AGENTS.md references via each runtime's native mechanism:
|
|
838
|
-
# OpenCode (json-instructions), Claude (AGENTS.md autodiscovery), Codex, Cursor,
|
|
839
|
-
# Droid, Gemini, Windsurf, Continue, Kilo, Kiro, Aider.
|
|
840
|
-
local adapter_script="${INSTALL_DIR}/.agents/scripts/prompt-injection-adapter.sh"
|
|
841
|
-
|
|
842
|
-
if [[ -f "$adapter_script" ]]; then
|
|
843
|
-
# shellcheck source=/dev/null
|
|
844
|
-
source "$adapter_script"
|
|
845
|
-
deploy_prompts_for_all_runtimes
|
|
846
|
-
else
|
|
847
|
-
# Fallback: adapter not yet deployed — use legacy inline logic
|
|
848
|
-
# This path is only hit during initial setup before .agents/ is deployed.
|
|
849
|
-
print_warning "prompt-injection-adapter.sh not found — using legacy deployment"
|
|
850
|
-
_inject_agents_reference_legacy
|
|
851
|
-
fi
|
|
852
|
-
|
|
853
|
-
return 0
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
# Legacy fallback for inject_agents_reference — used only when the adapter
|
|
857
|
-
# script is not yet available (e.g., during initial setup before .agents/ deploy).
|
|
858
|
-
# Will be removed once t1665 migration is complete.
|
|
859
|
-
_inject_agents_reference_legacy() {
|
|
860
|
-
local reference_line="$_AIDEVOPS_REFERENCE_LINE"
|
|
861
|
-
|
|
862
|
-
# AI assistant agent directories - these receive AGENTS.md reference
|
|
863
|
-
local ai_agent_dirs=(
|
|
864
|
-
"$HOME/.claude:commands"
|
|
865
|
-
"$HOME/.opencode:."
|
|
866
|
-
)
|
|
867
|
-
|
|
868
|
-
local updated_count=0
|
|
869
|
-
|
|
870
|
-
for entry in "${ai_agent_dirs[@]}"; do
|
|
871
|
-
local config_dir="${entry%%:*}"
|
|
872
|
-
local agents_subdir="${entry##*:}"
|
|
873
|
-
local agents_dir="$config_dir/$agents_subdir"
|
|
874
|
-
local agents_file="$agents_dir/AGENTS.md"
|
|
875
|
-
|
|
876
|
-
# Only process if the config directory exists (tool is installed)
|
|
877
|
-
if [[ -d "$config_dir" ]]; then
|
|
878
|
-
mkdir -p "$agents_dir"
|
|
879
|
-
|
|
880
|
-
if [[ -f "$agents_file" ]]; then
|
|
881
|
-
local first_line
|
|
882
|
-
first_line=$(head -1 "$agents_file" 2>/dev/null || echo "")
|
|
883
|
-
if [[ "$first_line" != *"aidevops/agents/AGENTS.md"* ]]; then
|
|
884
|
-
local temp_file
|
|
885
|
-
temp_file=$(mktemp)
|
|
886
|
-
trap 'rm -f "${temp_file:-}"' RETURN
|
|
887
|
-
echo "$reference_line" >"$temp_file"
|
|
888
|
-
echo "" >>"$temp_file"
|
|
889
|
-
cat "$agents_file" >>"$temp_file"
|
|
890
|
-
mv "$temp_file" "$agents_file"
|
|
891
|
-
print_success "Added reference to $agents_file"
|
|
892
|
-
((++updated_count))
|
|
893
|
-
else
|
|
894
|
-
print_info "Reference already exists in $agents_file"
|
|
895
|
-
fi
|
|
896
|
-
else
|
|
897
|
-
echo "$reference_line" >"$agents_file"
|
|
898
|
-
print_success "Created $agents_file with aidevops reference"
|
|
899
|
-
((++updated_count))
|
|
900
|
-
fi
|
|
901
|
-
fi
|
|
902
|
-
done
|
|
903
|
-
|
|
904
|
-
if [[ $updated_count -eq 0 ]]; then
|
|
905
|
-
print_info "No AI assistant configs found to update (tools may not be installed yet)"
|
|
906
|
-
else
|
|
907
|
-
print_success "Updated $updated_count AI assistant configuration(s)"
|
|
908
|
-
fi
|
|
909
|
-
|
|
910
|
-
# Clean up stale AGENTS.md from OpenCode agent dir
|
|
911
|
-
rm -f "$HOME/.config/opencode/agent/AGENTS.md"
|
|
912
|
-
|
|
913
|
-
# Deploy OpenCode config-level AGENTS.md from managed template
|
|
914
|
-
local opencode_config_dir="$HOME/.config/opencode"
|
|
915
|
-
local opencode_config_agents="$opencode_config_dir/AGENTS.md"
|
|
916
|
-
local template_source="$INSTALL_DIR/templates/opencode-config-agents.md"
|
|
917
|
-
|
|
918
|
-
if [[ -d "$opencode_config_dir" && -f "$template_source" ]]; then
|
|
919
|
-
if [[ -f "$opencode_config_agents" ]]; then
|
|
920
|
-
if ! diff -q "$template_source" "$opencode_config_agents" &>/dev/null; then
|
|
921
|
-
create_backup_with_rotation "$opencode_config_agents" "opencode-agents"
|
|
922
|
-
fi
|
|
923
|
-
fi
|
|
924
|
-
if cp "$template_source" "$opencode_config_agents"; then
|
|
925
|
-
print_success "Deployed greeting template to $opencode_config_agents"
|
|
926
|
-
else
|
|
927
|
-
print_error "Failed to deploy greeting template to $opencode_config_agents"
|
|
928
|
-
fi
|
|
929
|
-
fi
|
|
930
|
-
|
|
931
|
-
# Deploy Codex instructions.md (Codex reads ~/.codex/instructions.md as system prompt)
|
|
932
|
-
_deploy_codex_instructions
|
|
933
|
-
|
|
934
|
-
# Deploy Cursor AGENTS.md (Cursor reads ~/.cursor/rules/*.md as context)
|
|
935
|
-
_deploy_cursor_agents_reference
|
|
936
|
-
|
|
937
|
-
# Deploy Droid AGENTS.md (Droid reads ~/.factory/skills/*.md as context)
|
|
938
|
-
_deploy_droid_agents_reference
|
|
939
|
-
|
|
940
|
-
return 0
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
# Deploy instructions.md to Codex config directory.
|
|
944
|
-
# Codex reads ~/.codex/instructions.md as its system-level instructions.
|
|
945
|
-
_deploy_codex_instructions() {
|
|
946
|
-
local codex_dir="$HOME/.codex"
|
|
947
|
-
local instructions_file="$codex_dir/instructions.md"
|
|
948
|
-
|
|
949
|
-
# Only deploy if Codex is installed or config dir exists
|
|
950
|
-
if [[ ! -d "$codex_dir" ]] && ! command -v codex >/dev/null 2>&1; then
|
|
951
|
-
return 0
|
|
952
|
-
fi
|
|
953
|
-
|
|
954
|
-
mkdir -p "$codex_dir"
|
|
955
|
-
|
|
956
|
-
local reference_content="$_AIDEVOPS_REFERENCE_LINE"
|
|
957
|
-
|
|
958
|
-
if [[ -f "$instructions_file" ]]; then
|
|
959
|
-
# shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
|
|
960
|
-
if grep -q '~/.aidevops/agents/AGENTS.md' "$instructions_file" 2>/dev/null; then
|
|
961
|
-
print_info "Codex instructions.md already has aidevops reference"
|
|
962
|
-
return 0
|
|
963
|
-
fi
|
|
964
|
-
# Prepend reference to existing instructions
|
|
965
|
-
local temp_file
|
|
966
|
-
temp_file=$(mktemp)
|
|
967
|
-
echo "$reference_content" >"$temp_file"
|
|
968
|
-
echo "" >>"$temp_file"
|
|
969
|
-
cat "$instructions_file" >>"$temp_file"
|
|
970
|
-
mv "$temp_file" "$instructions_file"
|
|
971
|
-
print_success "Added aidevops reference to $instructions_file"
|
|
972
|
-
else
|
|
973
|
-
echo "$reference_content" >"$instructions_file"
|
|
974
|
-
print_success "Created $instructions_file with aidevops reference"
|
|
975
|
-
fi
|
|
976
|
-
return 0
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
# Deploy AGENTS.md reference to Cursor rules directory.
|
|
980
|
-
# Cursor reads ~/.cursor/rules/*.md files as additional context.
|
|
981
|
-
_deploy_cursor_agents_reference() {
|
|
982
|
-
local cursor_dir="$HOME/.cursor"
|
|
983
|
-
local rules_dir="$cursor_dir/rules"
|
|
984
|
-
local agents_file="$rules_dir/aidevops.md"
|
|
985
|
-
|
|
986
|
-
# Only deploy if Cursor is installed or config dir exists
|
|
987
|
-
if [[ ! -d "$cursor_dir" ]] && ! command -v cursor >/dev/null 2>&1 && ! command -v agent >/dev/null 2>&1; then
|
|
988
|
-
return 0
|
|
989
|
-
fi
|
|
990
|
-
|
|
991
|
-
mkdir -p "$rules_dir"
|
|
992
|
-
|
|
993
|
-
local reference_content="$_AIDEVOPS_REFERENCE_LINE"
|
|
994
|
-
|
|
995
|
-
if [[ -f "$agents_file" ]]; then
|
|
996
|
-
# shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
|
|
997
|
-
if grep -q '~/.aidevops/agents/AGENTS.md' "$agents_file" 2>/dev/null; then
|
|
998
|
-
print_info "Cursor rules/aidevops.md already has aidevops reference"
|
|
999
|
-
return 0
|
|
1000
|
-
fi
|
|
1001
|
-
fi
|
|
1002
|
-
|
|
1003
|
-
echo "$reference_content" >"$agents_file"
|
|
1004
|
-
print_success "Deployed aidevops reference to $agents_file"
|
|
1005
|
-
return 0
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
# Deploy AGENTS.md reference to Droid skills directory.
|
|
1009
|
-
# Droid reads ~/.factory/skills/*.md files as additional context.
|
|
1010
|
-
_deploy_droid_agents_reference() {
|
|
1011
|
-
local factory_dir="$HOME/.factory"
|
|
1012
|
-
local skills_dir="$factory_dir/skills"
|
|
1013
|
-
local agents_file="$skills_dir/aidevops.md"
|
|
1014
|
-
|
|
1015
|
-
# Only deploy if Droid is installed or config dir exists
|
|
1016
|
-
if [[ ! -d "$factory_dir" ]] && ! command -v droid >/dev/null 2>&1; then
|
|
1017
|
-
return 0
|
|
1018
|
-
fi
|
|
1019
|
-
|
|
1020
|
-
mkdir -p "$skills_dir"
|
|
1021
|
-
|
|
1022
|
-
local reference_content="$_AIDEVOPS_REFERENCE_LINE"
|
|
1023
|
-
|
|
1024
|
-
if [[ -f "$agents_file" ]]; then
|
|
1025
|
-
# shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
|
|
1026
|
-
if grep -q '~/.aidevops/agents/AGENTS.md' "$agents_file" 2>/dev/null; then
|
|
1027
|
-
print_info "Droid skills/aidevops.md already has aidevops reference"
|
|
1028
|
-
return 0
|
|
1029
|
-
fi
|
|
1030
|
-
fi
|
|
1031
|
-
|
|
1032
|
-
echo "$reference_content" >"$agents_file"
|
|
1033
|
-
print_success "Deployed aidevops reference to $agents_file"
|
|
1034
|
-
return 0
|
|
1035
|
-
}
|