aidevops 3.5.892 → 3.8.2
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 +96 -16
- package/VERSION +1 -1
- package/aidevops.sh +293 -34
- package/package.json +1 -1
- package/setup-modules/agent-deploy.sh +297 -669
- package/setup-modules/agent-runtime.sh +251 -0
- package/setup-modules/config.sh +83 -0
- package/setup-modules/core.sh +2 -0
- package/setup-modules/mcp-setup.sh +2 -0
- package/setup-modules/migrations.sh +267 -1
- package/setup-modules/plugins.sh +2 -0
- package/setup-modules/post-setup.sh +8 -1
- package/setup-modules/schedulers.sh +762 -336
- package/setup-modules/shell-env.sh +54 -16
- package/setup-modules/tool-beads.sh +324 -0
- package/setup-modules/tool-install.sh +6 -1
- package/setup.sh +207 -2
- package/templates/deploy-templates.sh +2 -0
- package/templates/home/.agents/README.md +3 -0
- package/templates/home/AGENTS.md +3 -0
- package/templates/home/git/.agent/README.md +3 -0
- package/templates/home/git/AGENTS.md +3 -0
- package/templates/opencode-config-agents.md +3 -0
- package/templates/standard-functions.sh +2 -0
- package/templates/wordpress-performance-workflow.md +3 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
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
|
|
3
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
|
|
4
7
|
|
|
5
8
|
# Shell safety baseline
|
|
6
9
|
set -Eeuo pipefail
|
|
@@ -9,11 +12,13 @@ IFS=$'\n\t'
|
|
|
9
12
|
trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
|
|
10
13
|
shopt -s inherit_errexit 2>/dev/null || true
|
|
11
14
|
|
|
15
|
+
# Shared reference line injected into all runtime agent configs
|
|
16
|
+
readonly _AIDEVOPS_REFERENCE_LINE='Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities.'
|
|
17
|
+
|
|
12
18
|
deploy_ai_templates() {
|
|
13
19
|
print_info "Deploying AI assistant templates..."
|
|
14
20
|
|
|
15
21
|
if [[ -f "templates/deploy-templates.sh" ]]; then
|
|
16
|
-
print_info "Running template deployment script..."
|
|
17
22
|
if bash templates/deploy-templates.sh; then
|
|
18
23
|
print_success "AI assistant templates deployed successfully"
|
|
19
24
|
else
|
|
@@ -26,7 +31,7 @@ deploy_ai_templates() {
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
extract_opencode_prompts() {
|
|
29
|
-
local extract_script="
|
|
34
|
+
local extract_script="${INSTALL_DIR:-.}/.agents/scripts/extract-opencode-prompts.sh"
|
|
30
35
|
if [[ -f "$extract_script" ]]; then
|
|
31
36
|
if bash "$extract_script"; then
|
|
32
37
|
print_success "OpenCode prompts extracted"
|
|
@@ -38,7 +43,7 @@ extract_opencode_prompts() {
|
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
check_opencode_prompt_drift() {
|
|
41
|
-
local drift_script="
|
|
46
|
+
local drift_script="${INSTALL_DIR:-.}/.agents/scripts/opencode-prompt-drift-check.sh"
|
|
42
47
|
if [[ -f "$drift_script" ]]; then
|
|
43
48
|
local output exit_code=0
|
|
44
49
|
# 2>/dev/null is intentional: --quiet mode suppresses expected output; all exit
|
|
@@ -60,44 +65,6 @@ check_opencode_prompt_drift() {
|
|
|
60
65
|
return 0
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
# _deploy_agents_clean_mode target_dir [preserved_dirs...]
|
|
64
|
-
# Removes stale files from target_dir while preserving listed subdirectories.
|
|
65
|
-
_deploy_agents_clean_mode() {
|
|
66
|
-
local target_dir="$1"
|
|
67
|
-
shift
|
|
68
|
-
local -a preserved_dirs=("$@")
|
|
69
|
-
|
|
70
|
-
print_info "Clean mode: removing stale files from $target_dir (preserving ${preserved_dirs[*]})"
|
|
71
|
-
local tmp_preserve
|
|
72
|
-
tmp_preserve="$(mktemp -d)"
|
|
73
|
-
trap 'rm -rf "${tmp_preserve:-}"' RETURN
|
|
74
|
-
if [[ -z "$tmp_preserve" || ! -d "$tmp_preserve" ]]; then
|
|
75
|
-
print_error "Failed to create temp dir for preserving agents"
|
|
76
|
-
return 1
|
|
77
|
-
fi
|
|
78
|
-
local preserve_failed=false
|
|
79
|
-
for pdir in "${preserved_dirs[@]}"; do
|
|
80
|
-
if [[ -d "$target_dir/$pdir" ]]; then
|
|
81
|
-
if ! cp -R "$target_dir/$pdir" "$tmp_preserve/$pdir"; then
|
|
82
|
-
preserve_failed=true
|
|
83
|
-
fi
|
|
84
|
-
fi
|
|
85
|
-
done
|
|
86
|
-
if [[ "$preserve_failed" == "true" ]]; then
|
|
87
|
-
print_error "Failed to preserve user/plugin agents; aborting clean"
|
|
88
|
-
rm -rf "$tmp_preserve"
|
|
89
|
-
return 1
|
|
90
|
-
fi
|
|
91
|
-
rm -rf "${target_dir:?}"/*
|
|
92
|
-
for pdir in "${preserved_dirs[@]}"; do
|
|
93
|
-
if [[ -d "$tmp_preserve/$pdir" ]]; then
|
|
94
|
-
cp -R "$tmp_preserve/$pdir" "$target_dir/$pdir"
|
|
95
|
-
fi
|
|
96
|
-
done
|
|
97
|
-
rm -rf "$tmp_preserve"
|
|
98
|
-
return 0
|
|
99
|
-
}
|
|
100
|
-
|
|
101
68
|
# _deploy_agents_copy source_dir target_dir [plugin_namespaces...]
|
|
102
69
|
# Copies agent files using rsync (preferred) or tar fallback.
|
|
103
70
|
# Returns 0 on success, 1 on failure.
|
|
@@ -165,33 +132,158 @@ _inject_plan_reminder() {
|
|
|
165
132
|
return 0
|
|
166
133
|
}
|
|
167
134
|
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
|
|
135
|
+
# _resolve_model_tiers_in_frontmatter target_dir
|
|
136
|
+
# Resolves tier shorthands (sonnet, haiku, opus, etc.) in YAML frontmatter
|
|
137
|
+
# `model:` fields to fully-qualified provider/model IDs using model-routing-table.json.
|
|
138
|
+
# This enables runtimes like OpenCode that consume `model:` literally (GH#18043).
|
|
139
|
+
# Source .md files keep tier names; deployed files get FQIDs.
|
|
140
|
+
# Only processes files with YAML frontmatter (--- delimited) where `model:` contains
|
|
141
|
+
# a bare tier name (no `/`). Already-qualified IDs are left unchanged.
|
|
142
|
+
_resolve_model_tiers_in_frontmatter() {
|
|
143
|
+
local target_dir="$1"
|
|
144
|
+
|
|
145
|
+
# Locate routing tables: merge custom overrides with default
|
|
146
|
+
local default_table="$target_dir/configs/model-routing-table.json"
|
|
147
|
+
local custom_table="$target_dir/custom/configs/model-routing-table.json"
|
|
148
|
+
|
|
149
|
+
if [[ ! -f "$default_table" ]]; then
|
|
150
|
+
print_warning "model-routing-table.json not found — skipping frontmatter model resolution"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# Requires jq for JSON parsing
|
|
155
|
+
if ! command -v jq &>/dev/null; then
|
|
156
|
+
print_warning "jq not available — skipping frontmatter model resolution"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Build a sed script file from the routing table(s) in ONE jq call.
|
|
161
|
+
# Custom table overrides specific tiers; default fills in the rest.
|
|
162
|
+
# Each line is a separate sed command for cross-platform compatibility
|
|
163
|
+
# (macOS sed doesn't support ; as command separator inside {}).
|
|
164
|
+
# Generates replacements for both plain and commented forms:
|
|
165
|
+
# model: sonnet → model: anthropic/claude-sonnet-4-6
|
|
166
|
+
# model: sonnet # ... → model: anthropic/claude-sonnet-4-6 # ...
|
|
167
|
+
local sed_file
|
|
168
|
+
sed_file=$(mktemp "${TMPDIR:-/tmp}/model-resolve-XXXXXX.sed")
|
|
169
|
+
if [[ -f "$custom_table" ]]; then
|
|
170
|
+
# Merge: custom tiers override default tiers (jq * operator)
|
|
171
|
+
jq -r -s '
|
|
172
|
+
(.[0].tiers // {}) * (.[1].tiers // {}) |
|
|
173
|
+
to_entries[] |
|
|
174
|
+
"s|^model: \(.key)$|model: \(.value.models[0])|",
|
|
175
|
+
"s|^model: \(.key) #|model: \(.value.models[0]) #|"
|
|
176
|
+
' "$default_table" "$custom_table" >"$sed_file" 2>/dev/null
|
|
177
|
+
else
|
|
178
|
+
jq -r '
|
|
179
|
+
.tiers | to_entries[] |
|
|
180
|
+
"s|^model: \(.key)$|model: \(.value.models[0])|",
|
|
181
|
+
"s|^model: \(.key) #|model: \(.value.models[0]) #|"
|
|
182
|
+
' "$default_table" >"$sed_file" 2>/dev/null
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [[ ! -s "$sed_file" ]]; then
|
|
186
|
+
rm -f "$sed_file"
|
|
187
|
+
print_warning "No tiers found in routing table — skipping frontmatter model resolution"
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Build a grep pattern to find only files with bare tier names.
|
|
192
|
+
# This avoids scanning all 3000+ .md files — only ~60 need changes.
|
|
193
|
+
# Extract tier names from the sed file (each line has the tier name after "model: ")
|
|
194
|
+
local tier_names
|
|
195
|
+
if [[ -f "$custom_table" ]]; then
|
|
196
|
+
tier_names=$(jq -r -s '(.[0].tiers // {}) * (.[1].tiers // {}) | keys[]' "$default_table" "$custom_table" 2>/dev/null | paste -sd'|' -)
|
|
197
|
+
else
|
|
198
|
+
tier_names=$(jq -r '.tiers | keys[]' "$default_table" 2>/dev/null | paste -sd'|' -)
|
|
199
|
+
fi
|
|
200
|
+
if [[ -z "$tier_names" ]]; then
|
|
201
|
+
rm -f "$sed_file"
|
|
202
|
+
return 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# Find candidate files: have a model: line with a bare tier name (no /)
|
|
206
|
+
# grep -rl is fast — scans content without loading full files
|
|
207
|
+
# The || true prevents set -e from exiting when grep finds no matches
|
|
208
|
+
local md_file
|
|
209
|
+
{ grep -rlE "^model: ($tier_names)(\$| #)" "$target_dir" --include='*.md' 2>/dev/null || true; } | while IFS= read -r md_file; do
|
|
210
|
+
[[ -n "$md_file" ]] || continue
|
|
211
|
+
# Verify the match is in YAML frontmatter (first line is ---)
|
|
212
|
+
local first_line
|
|
213
|
+
first_line=$(head -1 "$md_file" 2>/dev/null) || continue
|
|
214
|
+
[[ "$first_line" == "---" ]] || continue
|
|
215
|
+
|
|
216
|
+
# Apply sed replacements from the script file (macOS sed -i '' vs GNU sed -i)
|
|
217
|
+
sed -i '' -f "$sed_file" "$md_file" 2>/dev/null ||
|
|
218
|
+
sed -i -f "$sed_file" "$md_file" 2>/dev/null || true
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
# Count remaining unresolved files
|
|
222
|
+
local remaining
|
|
223
|
+
remaining=$({ grep -rlE "^model: ($tier_names)(\$| #)" "$target_dir" --include='*.md' 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
224
|
+
if [[ "$remaining" -eq 0 ]]; then
|
|
225
|
+
print_success "Resolved model tiers to FQIDs in deployed agent files (via model-routing-table.json)"
|
|
226
|
+
else
|
|
227
|
+
print_warning "Some model tiers could not be resolved ($remaining files remaining)"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
rm -f "$sed_file"
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# _set_script_permissions_and_report target_dir
|
|
235
|
+
# Sets execute permissions on all deployed scripts and reports deployed counts.
|
|
236
|
+
_set_script_permissions_and_report() {
|
|
172
237
|
local target_dir="$1"
|
|
173
|
-
local repo_dir="$2"
|
|
174
|
-
local source_dir="$3"
|
|
175
|
-
local plugins_file="$4"
|
|
176
238
|
|
|
177
|
-
# Set permissions on scripts (top-level and modularised subdirectories)
|
|
178
239
|
chmod +x "$target_dir/scripts/"*.sh 2>/dev/null || true
|
|
179
240
|
find "$target_dir/scripts" -mindepth 2 -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
|
180
241
|
|
|
181
|
-
# Count what was deployed
|
|
182
242
|
local agent_count script_count
|
|
183
243
|
agent_count=$(find "$target_dir" -name "*.md" -type f | wc -l | tr -d ' ')
|
|
184
244
|
script_count=$(find "$target_dir/scripts" -name "*.sh" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
185
245
|
print_info "Deployed $agent_count agent files and $script_count scripts"
|
|
246
|
+
return 0
|
|
247
|
+
}
|
|
186
248
|
|
|
187
|
-
|
|
249
|
+
# _install_opencode_plugin_deps target_dir
|
|
250
|
+
# Installs node_modules for the opencode-aidevops plugin.
|
|
251
|
+
# GH#17829: @bufbuild/protobuf was missing; GH#17891: only symlink on first run.
|
|
252
|
+
# Uses --omit=peer to skip the 630MB opencode-ai peer dep (the host app).
|
|
253
|
+
_install_opencode_plugin_deps() {
|
|
254
|
+
local target_dir="$1"
|
|
188
255
|
local oc_node_modules="$HOME/.config/opencode/node_modules"
|
|
189
256
|
local plugin_dir="$target_dir/plugins/opencode-aidevops"
|
|
190
|
-
|
|
191
|
-
|
|
257
|
+
|
|
258
|
+
if [[ ! -d "$plugin_dir" ]]; then
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# Only symlink if node_modules doesn't exist at all (first run)
|
|
263
|
+
if [[ ! -e "$plugin_dir/node_modules" ]]; then
|
|
264
|
+
if [[ -d "$oc_node_modules" ]]; then
|
|
265
|
+
ln -sf "$oc_node_modules" "$plugin_dir/node_modules" 2>/dev/null || true
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Verify critical dependency is available; npm install if not
|
|
270
|
+
if [[ ! -d "$plugin_dir/node_modules/@bufbuild/protobuf" ]]; then
|
|
271
|
+
if command -v npm &>/dev/null; then
|
|
272
|
+
# Remove symlink if present so npm creates a local node_modules
|
|
273
|
+
[[ -L "$plugin_dir/node_modules" ]] && rm "$plugin_dir/node_modules"
|
|
274
|
+
npm install --omit=dev --omit=peer --prefix "$plugin_dir" >/dev/null 2>&1 ||
|
|
275
|
+
print_warning "Failed to install plugin dependencies (non-blocking)"
|
|
276
|
+
fi
|
|
192
277
|
fi
|
|
278
|
+
return 0
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# _deploy_version_file target_dir repo_dir
|
|
282
|
+
# Copies VERSION file from repo root to the deployed agents directory.
|
|
283
|
+
_deploy_version_file() {
|
|
284
|
+
local target_dir="$1"
|
|
285
|
+
local repo_dir="$2"
|
|
193
286
|
|
|
194
|
-
# Copy VERSION file from repo root to deployed agents
|
|
195
287
|
if [[ -f "$repo_dir/VERSION" ]]; then
|
|
196
288
|
if cp "$repo_dir/VERSION" "$target_dir/VERSION"; then
|
|
197
289
|
print_info "Copied VERSION file to deployed agents"
|
|
@@ -201,30 +293,40 @@ _deploy_agents_post_copy() {
|
|
|
201
293
|
else
|
|
202
294
|
print_warning "VERSION file not found in repo root"
|
|
203
295
|
fi
|
|
296
|
+
return 0
|
|
297
|
+
}
|
|
204
298
|
|
|
205
|
-
|
|
299
|
+
# _deploy_security_advisories_files source_dir
|
|
300
|
+
# Copies *.advisory files to ~/.aidevops/advisories/ (shown in session greeting).
|
|
301
|
+
_deploy_security_advisories_files() {
|
|
302
|
+
local source_dir="$1"
|
|
206
303
|
local advisories_source="$source_dir/advisories"
|
|
207
304
|
local advisories_target="$HOME/.aidevops/advisories"
|
|
208
|
-
if [[ -d "$advisories_source" ]]; then
|
|
209
|
-
mkdir -p "$advisories_target"
|
|
210
|
-
local adv_count=0
|
|
211
|
-
for adv_file in "$advisories_source"/*.advisory; do
|
|
212
|
-
[[ -f "$adv_file" ]] || continue
|
|
213
|
-
cp "$adv_file" "$advisories_target/"
|
|
214
|
-
adv_count=$((adv_count + 1))
|
|
215
|
-
done
|
|
216
|
-
if [[ "$adv_count" -gt 0 ]]; then
|
|
217
|
-
print_info "Deployed $adv_count security advisory/advisories"
|
|
218
|
-
fi
|
|
219
|
-
fi
|
|
220
305
|
|
|
221
|
-
|
|
222
|
-
|
|
306
|
+
if [[ ! -d "$advisories_source" ]]; then
|
|
307
|
+
return 0
|
|
308
|
+
fi
|
|
309
|
+
mkdir -p "$advisories_target"
|
|
310
|
+
local adv_count=0
|
|
311
|
+
for adv_file in "$advisories_source"/*.advisory; do
|
|
312
|
+
[[ -f "$adv_file" ]] || continue
|
|
313
|
+
cp "$adv_file" "$advisories_target/"
|
|
314
|
+
adv_count=$((adv_count + 1))
|
|
315
|
+
done
|
|
316
|
+
if [[ "$adv_count" -gt 0 ]]; then
|
|
317
|
+
print_info "Deployed $adv_count security advisory/advisories"
|
|
318
|
+
fi
|
|
319
|
+
return 0
|
|
320
|
+
}
|
|
223
321
|
|
|
224
|
-
|
|
322
|
+
# _migrate_mailbox_if_needed target_dir
|
|
323
|
+
# Migrates mailbox from legacy TOON files to SQLite if old files exist.
|
|
324
|
+
_migrate_mailbox_if_needed() {
|
|
325
|
+
local target_dir="$1"
|
|
225
326
|
local aidevops_workspace_dir="${AIDEVOPS_WORKSPACE_DIR:-$HOME/.aidevops/.agent-workspace}"
|
|
226
327
|
local mail_dir="${AIDEVOPS_MAIL_DIR:-${aidevops_workspace_dir}/mail}"
|
|
227
328
|
local mail_script="$target_dir/scripts/mail-helper.sh"
|
|
329
|
+
|
|
228
330
|
if [[ -x "$mail_script" ]] && find "$mail_dir" -name "*.toon" 2>/dev/null | grep -q .; then
|
|
229
331
|
if "$mail_script" migrate; then
|
|
230
332
|
print_success "Mailbox migration complete"
|
|
@@ -232,20 +334,89 @@ _deploy_agents_post_copy() {
|
|
|
232
334
|
print_warning "Mailbox migration had issues (non-critical, old files preserved)"
|
|
233
335
|
fi
|
|
234
336
|
fi
|
|
337
|
+
return 0
|
|
338
|
+
}
|
|
235
339
|
|
|
236
|
-
|
|
340
|
+
# _migrate_wavespeed_md target_dir
|
|
341
|
+
# Removes stale wavespeed.md from deprecated services/ai-generation/ path (v2.111+).
|
|
342
|
+
_migrate_wavespeed_md() {
|
|
343
|
+
local target_dir="$1"
|
|
237
344
|
local old_wavespeed="$target_dir/services/ai-generation/wavespeed.md"
|
|
345
|
+
|
|
238
346
|
if [[ -f "$old_wavespeed" ]]; then
|
|
239
347
|
rm -f "$old_wavespeed"
|
|
240
348
|
rmdir "$target_dir/services/ai-generation" 2>/dev/null || true
|
|
241
349
|
print_info "Migrated wavespeed.md from services/ai-generation/ to tools/video/"
|
|
242
350
|
fi
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# _deploy_agents_post_copy target_dir repo_dir source_dir plugins_file
|
|
355
|
+
# Orchestrates all post-copy steps: permissions, VERSION, advisories, plan-reminder,
|
|
356
|
+
# mailbox migration, stale-file migration, model resolution, and plugin deployment.
|
|
357
|
+
_deploy_agents_post_copy() {
|
|
358
|
+
local target_dir="$1"
|
|
359
|
+
local repo_dir="$2"
|
|
360
|
+
local source_dir="$3"
|
|
361
|
+
local plugins_file="$4"
|
|
243
362
|
|
|
244
|
-
|
|
363
|
+
_set_script_permissions_and_report "$target_dir"
|
|
364
|
+
_install_opencode_plugin_deps "$target_dir"
|
|
365
|
+
_deploy_version_file "$target_dir" "$repo_dir"
|
|
366
|
+
_deploy_security_advisories_files "$source_dir"
|
|
367
|
+
_inject_plan_reminder "$target_dir"
|
|
368
|
+
_migrate_mailbox_if_needed "$target_dir"
|
|
369
|
+
_migrate_wavespeed_md "$target_dir"
|
|
370
|
+
# Source files keep tier names (sonnet, haiku, opus); deployed files get
|
|
371
|
+
# fully-qualified IDs (anthropic/claude-sonnet-4-6) that runtimes like
|
|
372
|
+
# OpenCode can consume directly (GH#18043).
|
|
373
|
+
_resolve_model_tiers_in_frontmatter "$target_dir"
|
|
245
374
|
deploy_plugins "$target_dir" "$plugins_file"
|
|
246
375
|
return 0
|
|
247
376
|
}
|
|
248
377
|
|
|
378
|
+
# _warn_deployed_script_drift source_dir target_dir
|
|
379
|
+
# Compares deployed scripts against canonical source and warns if any differ.
|
|
380
|
+
# This catches the case where someone edited ~/.aidevops/agents/scripts/ directly
|
|
381
|
+
# (those edits are overwritten by every deploy). Emits a warning listing drifted
|
|
382
|
+
# files and the canonical source path to edit instead.
|
|
383
|
+
# Non-fatal: always returns 0 so deployment proceeds.
|
|
384
|
+
_warn_deployed_script_drift() {
|
|
385
|
+
local source_dir="$1"
|
|
386
|
+
local target_dir="$2"
|
|
387
|
+
local source_scripts="$source_dir/scripts"
|
|
388
|
+
local target_scripts="$target_dir/scripts"
|
|
389
|
+
|
|
390
|
+
if [[ ! -d "$source_scripts" || ! -d "$target_scripts" ]]; then
|
|
391
|
+
return 0
|
|
392
|
+
fi
|
|
393
|
+
if ! command -v diff &>/dev/null; then
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
local -a drifted=()
|
|
398
|
+
local f bn
|
|
399
|
+
for f in "$target_scripts"/*.sh; do
|
|
400
|
+
[[ -f "$f" ]] || continue
|
|
401
|
+
bn=$(basename "$f")
|
|
402
|
+
local src="$source_scripts/$bn"
|
|
403
|
+
if [[ -f "$src" ]] && ! diff -q "$src" "$f" &>/dev/null; then
|
|
404
|
+
drifted+=("$bn")
|
|
405
|
+
fi
|
|
406
|
+
done
|
|
407
|
+
|
|
408
|
+
if [[ ${#drifted[@]} -gt 0 ]]; then
|
|
409
|
+
print_warning "Deployed scripts differ from canonical source (local edits will be overwritten; backup will be created):"
|
|
410
|
+
for bn in "${drifted[@]}"; do
|
|
411
|
+
print_warning " $target_scripts/$bn"
|
|
412
|
+
print_warning " → canonical: $source_scripts/$bn"
|
|
413
|
+
done
|
|
414
|
+
print_warning "To keep personal scripts: use $target_dir/custom/scripts/"
|
|
415
|
+
print_warning "To fix the canonical source: edit $source_scripts/ and re-run setup.sh"
|
|
416
|
+
fi
|
|
417
|
+
return 0
|
|
418
|
+
}
|
|
419
|
+
|
|
249
420
|
deploy_aidevops_agents() {
|
|
250
421
|
print_info "Deploying aidevops agents to ~/.aidevops/agents/..."
|
|
251
422
|
|
|
@@ -278,47 +449,71 @@ deploy_aidevops_agents() {
|
|
|
278
449
|
done < <(jq -r '.plugins[].namespace // empty' "$plugins_file" 2>/dev/null)
|
|
279
450
|
fi
|
|
280
451
|
|
|
452
|
+
# Warn if deployed scripts have been locally modified (GH#17414).
|
|
453
|
+
# These edits will be overwritten — users must edit the canonical source.
|
|
454
|
+
if [[ -d "$target_dir" ]]; then
|
|
455
|
+
_warn_deployed_script_drift "$source_dir" "$target_dir"
|
|
456
|
+
fi
|
|
457
|
+
|
|
281
458
|
# Create backup if target exists (with rotation)
|
|
282
459
|
if [[ -d "$target_dir" ]]; then
|
|
283
460
|
create_backup_with_rotation "$target_dir" "agents"
|
|
284
461
|
fi
|
|
285
462
|
|
|
286
|
-
# Create target directory
|
|
287
463
|
mkdir -p "$target_dir"
|
|
288
464
|
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
#
|
|
303
|
-
# - custom/ (user's private agents, never overwritten)
|
|
304
|
-
# - draft/ (user's experimental agents, never overwritten)
|
|
305
|
-
# - plugin namespace directories (managed separately)
|
|
465
|
+
# Atomic deploy: build a staging directory, then swap it into place.
|
|
466
|
+
# Previously, clean + copy happened in-place, creating a window where
|
|
467
|
+
# scripts were missing. The pulse could dispatch workers mid-deploy,
|
|
468
|
+
# hitting "No such file or directory" errors. Now we:
|
|
469
|
+
# 1. rsync into a staging dir (target_dir.staging)
|
|
470
|
+
# 2. Move preserved dirs (custom/, draft/, plugins) from live to staging
|
|
471
|
+
# 3. mv live → .old, mv staging → live (atomic on same filesystem)
|
|
472
|
+
# 4. rm .old
|
|
473
|
+
local staging_dir="${target_dir}.staging"
|
|
474
|
+
local old_dir="${target_dir}.old"
|
|
475
|
+
rm -rf "$staging_dir" "$old_dir"
|
|
476
|
+
mkdir -p "$staging_dir"
|
|
477
|
+
|
|
478
|
+
# Copy source into staging
|
|
306
479
|
local copy_rc
|
|
307
480
|
if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
|
|
308
|
-
_deploy_agents_copy "$source_dir" "$
|
|
481
|
+
_deploy_agents_copy "$source_dir" "$staging_dir" "${plugin_namespaces[@]}"
|
|
309
482
|
copy_rc=$?
|
|
310
483
|
else
|
|
311
|
-
_deploy_agents_copy "$source_dir" "$
|
|
484
|
+
_deploy_agents_copy "$source_dir" "$staging_dir"
|
|
312
485
|
copy_rc=$?
|
|
313
486
|
fi
|
|
314
|
-
if [[ "$copy_rc" -
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
else
|
|
318
|
-
print_error "Failed to deploy agents"
|
|
487
|
+
if [[ "$copy_rc" -ne 0 ]]; then
|
|
488
|
+
print_error "Failed to deploy agents to staging directory"
|
|
489
|
+
rm -rf "$staging_dir"
|
|
319
490
|
return 1
|
|
320
491
|
fi
|
|
321
492
|
|
|
493
|
+
# Carry over preserved directories from live target to staging
|
|
494
|
+
local -a preserved_dirs=("custom" "draft")
|
|
495
|
+
if [[ ${#plugin_namespaces[@]} -gt 0 ]]; then
|
|
496
|
+
for pns in "${plugin_namespaces[@]}"; do
|
|
497
|
+
preserved_dirs+=("$pns")
|
|
498
|
+
done
|
|
499
|
+
fi
|
|
500
|
+
for pdir in "${preserved_dirs[@]}"; do
|
|
501
|
+
if [[ -d "$target_dir/$pdir" ]]; then
|
|
502
|
+
# Move user dirs into staging so they survive the swap
|
|
503
|
+
cp -a "$target_dir/$pdir" "$staging_dir/$pdir" 2>/dev/null || true
|
|
504
|
+
fi
|
|
505
|
+
done
|
|
506
|
+
|
|
507
|
+
# Atomic swap: mv is atomic on the same filesystem (POSIX rename())
|
|
508
|
+
if [[ -d "$target_dir" ]]; then
|
|
509
|
+
mv "$target_dir" "$old_dir"
|
|
510
|
+
fi
|
|
511
|
+
mv "$staging_dir" "$target_dir"
|
|
512
|
+
rm -rf "$old_dir"
|
|
513
|
+
|
|
514
|
+
print_success "Deployed agents to $target_dir"
|
|
515
|
+
_deploy_agents_post_copy "$target_dir" "$repo_dir" "$source_dir" "$plugins_file"
|
|
516
|
+
|
|
322
517
|
return 0
|
|
323
518
|
}
|
|
324
519
|
|
|
@@ -349,7 +544,7 @@ inject_agents_reference() {
|
|
|
349
544
|
# script is not yet available (e.g., during initial setup before .agents/ deploy).
|
|
350
545
|
# Will be removed once t1665 migration is complete.
|
|
351
546
|
_inject_agents_reference_legacy() {
|
|
352
|
-
local reference_line=
|
|
547
|
+
local reference_line="$_AIDEVOPS_REFERENCE_LINE"
|
|
353
548
|
|
|
354
549
|
# AI assistant agent directories - these receive AGENTS.md reference
|
|
355
550
|
local ai_agent_dirs=(
|
|
@@ -445,11 +640,9 @@ _deploy_codex_instructions() {
|
|
|
445
640
|
|
|
446
641
|
mkdir -p "$codex_dir"
|
|
447
642
|
|
|
448
|
-
local reference_content
|
|
449
|
-
reference_content="Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities."
|
|
643
|
+
local reference_content="$_AIDEVOPS_REFERENCE_LINE"
|
|
450
644
|
|
|
451
645
|
if [[ -f "$instructions_file" ]]; then
|
|
452
|
-
# Check if our reference is already present
|
|
453
646
|
# shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
|
|
454
647
|
if grep -q '~/.aidevops/agents/AGENTS.md' "$instructions_file" 2>/dev/null; then
|
|
455
648
|
print_info "Codex instructions.md already has aidevops reference"
|
|
@@ -484,8 +677,7 @@ _deploy_cursor_agents_reference() {
|
|
|
484
677
|
|
|
485
678
|
mkdir -p "$rules_dir"
|
|
486
679
|
|
|
487
|
-
local reference_content
|
|
488
|
-
reference_content="Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities."
|
|
680
|
+
local reference_content="$_AIDEVOPS_REFERENCE_LINE"
|
|
489
681
|
|
|
490
682
|
if [[ -f "$agents_file" ]]; then
|
|
491
683
|
# shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
|
|
@@ -514,8 +706,7 @@ _deploy_droid_agents_reference() {
|
|
|
514
706
|
|
|
515
707
|
mkdir -p "$skills_dir"
|
|
516
708
|
|
|
517
|
-
local reference_content
|
|
518
|
-
reference_content="Add ~/.aidevops/agents/AGENTS.md to context for AI DevOps capabilities."
|
|
709
|
+
local reference_content="$_AIDEVOPS_REFERENCE_LINE"
|
|
519
710
|
|
|
520
711
|
if [[ -f "$agents_file" ]]; then
|
|
521
712
|
# shellcheck disable=SC2088 # Tilde is a literal grep pattern, not a path
|
|
@@ -529,566 +720,3 @@ _deploy_droid_agents_reference() {
|
|
|
529
720
|
print_success "Deployed aidevops reference to $agents_file"
|
|
530
721
|
return 0
|
|
531
722
|
}
|
|
532
|
-
|
|
533
|
-
# =============================================================================
|
|
534
|
-
# Deploy aidevops main agents to runtime-native agent directories
|
|
535
|
-
# =============================================================================
|
|
536
|
-
# Converts aidevops agents from ~/.aidevops/agents/*.md into each runtime's
|
|
537
|
-
# native agent format and copies them to the runtime's agent directory.
|
|
538
|
-
# Only deploys to installed runtimes that support agent directories.
|
|
539
|
-
#
|
|
540
|
-
# aidevops frontmatter fields stripped (not understood by target runtimes):
|
|
541
|
-
# mode, subagents
|
|
542
|
-
#
|
|
543
|
-
# Kept/mapped fields (compatible with Claude Code, Cursor, Amp):
|
|
544
|
-
# name, description, tools, model, permissionMode, hooks, mcpServers,
|
|
545
|
-
# maxTurns, initialPrompt, memory, background, isolation
|
|
546
|
-
|
|
547
|
-
# _convert_agent_frontmatter: strips aidevops-only fields from agent markdown.
|
|
548
|
-
# Reads from stdin, writes converted content to stdout.
|
|
549
|
-
# Tracks whether we're inside an indented block (subagents list) to correctly
|
|
550
|
-
# skip its child lines without stripping other indented YAML fields.
|
|
551
|
-
_convert_agent_frontmatter() {
|
|
552
|
-
local in_frontmatter=false
|
|
553
|
-
local frontmatter_started=false
|
|
554
|
-
local in_skip_block=false
|
|
555
|
-
local line_num=0
|
|
556
|
-
|
|
557
|
-
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
558
|
-
line_num=$((line_num + 1))
|
|
559
|
-
if [[ $line_num -eq 1 && "$line" == "---" ]]; then
|
|
560
|
-
in_frontmatter=true
|
|
561
|
-
frontmatter_started=true
|
|
562
|
-
echo "$line"
|
|
563
|
-
continue
|
|
564
|
-
fi
|
|
565
|
-
if [[ "$frontmatter_started" == "true" && "$in_frontmatter" == "true" && "$line" == "---" ]]; then
|
|
566
|
-
in_frontmatter=false
|
|
567
|
-
echo "$line"
|
|
568
|
-
continue
|
|
569
|
-
fi
|
|
570
|
-
if [[ "$in_frontmatter" == "true" ]]; then
|
|
571
|
-
# Detect top-level keys (no leading whitespace)
|
|
572
|
-
case "$line" in
|
|
573
|
-
mode:*)
|
|
574
|
-
in_skip_block=false
|
|
575
|
-
continue
|
|
576
|
-
;;
|
|
577
|
-
subagents:*)
|
|
578
|
-
in_skip_block=true
|
|
579
|
-
continue
|
|
580
|
-
;;
|
|
581
|
-
esac
|
|
582
|
-
# If inside a skipped block, consume indented continuation lines
|
|
583
|
-
if [[ "$in_skip_block" == "true" ]]; then
|
|
584
|
-
case "$line" in
|
|
585
|
-
[[:space:]]*)
|
|
586
|
-
# Indented line under a skipped key — skip it
|
|
587
|
-
continue
|
|
588
|
-
;;
|
|
589
|
-
*)
|
|
590
|
-
# Non-indented line — we've left the skip block
|
|
591
|
-
in_skip_block=false
|
|
592
|
-
echo "$line"
|
|
593
|
-
;;
|
|
594
|
-
esac
|
|
595
|
-
else
|
|
596
|
-
echo "$line"
|
|
597
|
-
fi
|
|
598
|
-
else
|
|
599
|
-
echo "$line"
|
|
600
|
-
fi
|
|
601
|
-
done
|
|
602
|
-
return 0
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
# _is_agent_definition: check if a markdown file has agent frontmatter (name: field).
|
|
606
|
-
# Returns 0 if the file is an agent definition, 1 otherwise.
|
|
607
|
-
_is_agent_definition() {
|
|
608
|
-
local file="$1"
|
|
609
|
-
# Check first 30 lines for name: in YAML frontmatter (fast path)
|
|
610
|
-
head -30 "$file" 2>/dev/null | grep -q '^name:' 2>/dev/null
|
|
611
|
-
return $?
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
# _agent_source_dirs: list directories under agents_source that contain subagents.
|
|
615
|
-
# Excludes framework infrastructure directories that are not agent definitions.
|
|
616
|
-
# Prints directory paths, one per line.
|
|
617
|
-
_agent_source_dirs() {
|
|
618
|
-
local agents_source="$1"
|
|
619
|
-
local dir
|
|
620
|
-
for dir in "$agents_source"/*/; do
|
|
621
|
-
[[ -d "$dir" ]] || continue
|
|
622
|
-
local dirname
|
|
623
|
-
dirname=$(basename "$dir")
|
|
624
|
-
# Skip framework infrastructure directories
|
|
625
|
-
case "$dirname" in
|
|
626
|
-
scripts | reference | prompts | templates | configs | hooks | \
|
|
627
|
-
plugins | bundles | loop-state | advisories | aidevops | \
|
|
628
|
-
custom | draft | tests | rules)
|
|
629
|
-
continue
|
|
630
|
-
;;
|
|
631
|
-
*)
|
|
632
|
-
echo "$dir"
|
|
633
|
-
;;
|
|
634
|
-
esac
|
|
635
|
-
done
|
|
636
|
-
return 0
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
# _collect_agent_files: print "abspath|relpath" lines for all deployable agent files
|
|
640
|
-
# under agents_source. Excludes AGENTS.md, SKILL.md stubs, and non-agent markdown.
|
|
641
|
-
# Output is consumed by deploy_agents_to_runtimes via a process substitution.
|
|
642
|
-
_collect_agent_files() {
|
|
643
|
-
local agents_source="$1"
|
|
644
|
-
local f bn
|
|
645
|
-
|
|
646
|
-
# Top-level agents
|
|
647
|
-
for f in "$agents_source"/*.md; do
|
|
648
|
-
[[ -f "$f" ]] || continue
|
|
649
|
-
bn=$(basename "$f")
|
|
650
|
-
[[ "$bn" == "AGENTS.md" ]] && continue
|
|
651
|
-
if _is_agent_definition "$f"; then
|
|
652
|
-
printf '%s|%s\n' "$f" "$bn"
|
|
653
|
-
fi
|
|
654
|
-
done
|
|
655
|
-
|
|
656
|
-
# Subagent directories (recursive)
|
|
657
|
-
local subdir
|
|
658
|
-
while IFS= read -r subdir; do
|
|
659
|
-
while IFS= read -r f; do
|
|
660
|
-
[[ -f "$f" ]] || continue
|
|
661
|
-
bn=$(basename "$f")
|
|
662
|
-
# Skip SKILL.md stubs — they're directory indexes, not real agents
|
|
663
|
-
[[ "$bn" == "SKILL.md" ]] && continue
|
|
664
|
-
if _is_agent_definition "$f"; then
|
|
665
|
-
local relpath="${f#"$agents_source"/}"
|
|
666
|
-
printf '%s|%s\n' "$f" "$relpath"
|
|
667
|
-
fi
|
|
668
|
-
done < <(find "$subdir" -name '*.md' -type f 2>/dev/null)
|
|
669
|
-
done < <(_agent_source_dirs "$agents_source")
|
|
670
|
-
return 0
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
# _deploy_agents_to_single_runtime: convert and copy all agent files to one runtime.
|
|
674
|
-
# Arguments: runtime_id agent_dir agent_list_file
|
|
675
|
-
# agent_list_file contains "abspath|relpath" lines produced by _collect_agent_files.
|
|
676
|
-
# Prints the count of successfully deployed agents to stdout.
|
|
677
|
-
_deploy_agents_to_single_runtime() {
|
|
678
|
-
local runtime_id="$1"
|
|
679
|
-
local agent_dir="$2"
|
|
680
|
-
local agent_list_file="$3"
|
|
681
|
-
|
|
682
|
-
# Only deploy if the runtime is actually installed
|
|
683
|
-
local binary config_path config_dir
|
|
684
|
-
binary=$(rt_binary "$runtime_id")
|
|
685
|
-
config_path=$(rt_config_path "$runtime_id")
|
|
686
|
-
config_dir="$(dirname "$config_path" 2>/dev/null)"
|
|
687
|
-
|
|
688
|
-
if ! type -P "$binary" >/dev/null 2>&1 && [[ ! -d "$config_dir" ]]; then
|
|
689
|
-
echo "0"
|
|
690
|
-
return 0
|
|
691
|
-
fi
|
|
692
|
-
|
|
693
|
-
mkdir -p "$agent_dir"
|
|
694
|
-
local agent_count=0
|
|
695
|
-
local src rel target target_parent
|
|
696
|
-
|
|
697
|
-
while IFS='|' read -r src rel; do
|
|
698
|
-
[[ -n "$src" && -n "$rel" ]] || continue
|
|
699
|
-
target="$agent_dir/$rel"
|
|
700
|
-
target_parent=$(dirname "$target")
|
|
701
|
-
[[ -d "$target_parent" ]] || mkdir -p "$target_parent"
|
|
702
|
-
if _convert_agent_frontmatter <"$src" >"$target"; then
|
|
703
|
-
agent_count=$((agent_count + 1))
|
|
704
|
-
fi
|
|
705
|
-
done <"$agent_list_file"
|
|
706
|
-
|
|
707
|
-
echo "$agent_count"
|
|
708
|
-
return 0
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
# deploy_agents_to_runtimes: main entry point called by setup.sh.
|
|
712
|
-
# Iterates all installed runtimes with agent directory support, converts and
|
|
713
|
-
# deploys aidevops agents (top-level and subagent directories) to each runtime's
|
|
714
|
-
# native agent directory. Only files with name: frontmatter are deployed.
|
|
715
|
-
# SKILL.md stubs (directory indexes) are excluded.
|
|
716
|
-
deploy_agents_to_runtimes() {
|
|
717
|
-
# Source runtime registry if not already loaded
|
|
718
|
-
local registry_script="${INSTALL_DIR:-.}/.agents/scripts/runtime-registry.sh"
|
|
719
|
-
if [[ -z "${_RUNTIME_REGISTRY_LOADED:-}" ]]; then
|
|
720
|
-
if [[ -f "$registry_script" ]]; then
|
|
721
|
-
# shellcheck source=/dev/null
|
|
722
|
-
source "$registry_script"
|
|
723
|
-
else
|
|
724
|
-
print_warning "Runtime registry not found — skipping agent deployment to runtimes"
|
|
725
|
-
return 0
|
|
726
|
-
fi
|
|
727
|
-
fi
|
|
728
|
-
|
|
729
|
-
local agents_source="${HOME}/.aidevops/agents"
|
|
730
|
-
if [[ ! -d "$agents_source" ]]; then
|
|
731
|
-
print_warning "No deployed agents found at $agents_source — skipping"
|
|
732
|
-
return 0
|
|
733
|
-
fi
|
|
734
|
-
|
|
735
|
-
# Build the agent file list once (shared across all runtimes) into a temp file.
|
|
736
|
-
# Each line: "abspath|relpath"
|
|
737
|
-
local agent_list_file
|
|
738
|
-
agent_list_file=$(mktemp)
|
|
739
|
-
trap 'rm -f "${agent_list_file:-}"' RETURN
|
|
740
|
-
_collect_agent_files "$agents_source" >"$agent_list_file"
|
|
741
|
-
|
|
742
|
-
local total_agents
|
|
743
|
-
total_agents=$(wc -l <"$agent_list_file" | tr -d ' ')
|
|
744
|
-
if [[ "$total_agents" -eq 0 ]]; then
|
|
745
|
-
print_warning "No agent definitions found in $agents_source"
|
|
746
|
-
return 0
|
|
747
|
-
fi
|
|
748
|
-
|
|
749
|
-
local deployed_count=0
|
|
750
|
-
local runtime_count=0
|
|
751
|
-
|
|
752
|
-
# Deploy to each installed runtime
|
|
753
|
-
local runtime_id agent_dir agent_count display_name
|
|
754
|
-
while IFS= read -r runtime_id; do
|
|
755
|
-
agent_dir=$(rt_agent_dir "$runtime_id")
|
|
756
|
-
[[ -z "$agent_dir" ]] && continue
|
|
757
|
-
|
|
758
|
-
agent_count=$(_deploy_agents_to_single_runtime "$runtime_id" "$agent_dir" "$agent_list_file")
|
|
759
|
-
# A count of 0 means runtime not installed (skipped) — don't increment runtime_count
|
|
760
|
-
if [[ "$agent_count" -gt 0 ]]; then
|
|
761
|
-
display_name=$(rt_display_name "$runtime_id")
|
|
762
|
-
print_info "Deployed $agent_count agents to $display_name ($agent_dir)"
|
|
763
|
-
deployed_count=$((deployed_count + agent_count))
|
|
764
|
-
runtime_count=$((runtime_count + 1))
|
|
765
|
-
fi
|
|
766
|
-
done < <(rt_list_with_agents)
|
|
767
|
-
|
|
768
|
-
if [[ $runtime_count -eq 0 ]]; then
|
|
769
|
-
print_info "No runtimes with agent directory support detected — skipping"
|
|
770
|
-
else
|
|
771
|
-
print_success "Deployed $deployed_count agent(s) across $runtime_count runtime(s)"
|
|
772
|
-
fi
|
|
773
|
-
|
|
774
|
-
return 0
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
install_beads_binary() {
|
|
778
|
-
local os arch tarball_name
|
|
779
|
-
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
780
|
-
arch=$(uname -m)
|
|
781
|
-
|
|
782
|
-
# Map architecture names to Beads release naming convention
|
|
783
|
-
case "$arch" in
|
|
784
|
-
x86_64 | amd64) arch="amd64" ;;
|
|
785
|
-
aarch64 | arm64) arch="arm64" ;;
|
|
786
|
-
*)
|
|
787
|
-
print_warning "Unsupported architecture for Beads binary download: $arch"
|
|
788
|
-
return 1
|
|
789
|
-
;;
|
|
790
|
-
esac
|
|
791
|
-
|
|
792
|
-
# Get latest version tag from GitHub API
|
|
793
|
-
local latest_version
|
|
794
|
-
latest_version=$(curl -fsSL "https://api.github.com/repos/steveyegge/beads/releases/latest" 2>/dev/null |
|
|
795
|
-
grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/')
|
|
796
|
-
|
|
797
|
-
if [[ -z "$latest_version" ]]; then
|
|
798
|
-
print_warning "Could not determine latest Beads version from GitHub"
|
|
799
|
-
return 1
|
|
800
|
-
fi
|
|
801
|
-
|
|
802
|
-
tarball_name="beads_${latest_version}_${os}_${arch}.tar.gz"
|
|
803
|
-
local download_url="https://github.com/steveyegge/beads/releases/download/v${latest_version}/${tarball_name}"
|
|
804
|
-
|
|
805
|
-
print_info "Downloading Beads CLI v${latest_version} (${os}/${arch})..."
|
|
806
|
-
|
|
807
|
-
local tmp_dir
|
|
808
|
-
tmp_dir=$(mktemp -d)
|
|
809
|
-
# shellcheck disable=SC2064 # Intentional: $tmp_dir must expand at trap definition time, not execution time
|
|
810
|
-
trap "rm -rf '$tmp_dir'" RETURN
|
|
811
|
-
|
|
812
|
-
if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name" 2>/dev/null; then
|
|
813
|
-
print_warning "Failed to download Beads binary from $download_url"
|
|
814
|
-
return 1
|
|
815
|
-
fi
|
|
816
|
-
|
|
817
|
-
# Extract and install
|
|
818
|
-
if ! tar -xzf "$tmp_dir/$tarball_name" -C "$tmp_dir" 2>/dev/null; then
|
|
819
|
-
print_warning "Failed to extract Beads binary"
|
|
820
|
-
return 1
|
|
821
|
-
fi
|
|
822
|
-
|
|
823
|
-
# Find the bd binary in the extracted files
|
|
824
|
-
local bd_binary
|
|
825
|
-
bd_binary=$(find "$tmp_dir" -name "bd" -type f 2>/dev/null | head -1)
|
|
826
|
-
if [[ -z "$bd_binary" ]]; then
|
|
827
|
-
print_warning "bd binary not found in downloaded archive"
|
|
828
|
-
return 1
|
|
829
|
-
fi
|
|
830
|
-
|
|
831
|
-
# Install to a writable location
|
|
832
|
-
local install_dir="/usr/local/bin"
|
|
833
|
-
if [[ ! -w "$install_dir" ]]; then
|
|
834
|
-
if command -v sudo &>/dev/null; then
|
|
835
|
-
sudo install -m 755 "$bd_binary" "$install_dir/bd"
|
|
836
|
-
else
|
|
837
|
-
# Fallback to user-local bin
|
|
838
|
-
install_dir="$HOME/.local/bin"
|
|
839
|
-
mkdir -p "$install_dir"
|
|
840
|
-
install -m 755 "$bd_binary" "$install_dir/bd"
|
|
841
|
-
# Ensure ~/.local/bin is in PATH
|
|
842
|
-
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
|
843
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
844
|
-
print_info "Added ~/.local/bin to PATH for this session"
|
|
845
|
-
fi
|
|
846
|
-
fi
|
|
847
|
-
else
|
|
848
|
-
install -m 755 "$bd_binary" "$install_dir/bd"
|
|
849
|
-
fi
|
|
850
|
-
|
|
851
|
-
if command -v bd &>/dev/null; then
|
|
852
|
-
print_success "Beads CLI installed via binary download (v${latest_version})"
|
|
853
|
-
return 0
|
|
854
|
-
else
|
|
855
|
-
print_warning "Beads binary installed to $install_dir/bd but not found in PATH"
|
|
856
|
-
return 1
|
|
857
|
-
fi
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
install_beads_go() {
|
|
861
|
-
if ! command -v go &>/dev/null; then
|
|
862
|
-
return 1
|
|
863
|
-
fi
|
|
864
|
-
if run_with_spinner "Installing Beads via Go" go install github.com/steveyegge/beads/cmd/bd@latest; then
|
|
865
|
-
print_info "Ensure \$GOPATH/bin is in your PATH"
|
|
866
|
-
return 0
|
|
867
|
-
fi
|
|
868
|
-
print_warning "Go installation failed"
|
|
869
|
-
return 1
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
setup_beads() {
|
|
873
|
-
print_info "Setting up Beads (task graph visualization)..."
|
|
874
|
-
|
|
875
|
-
# Check if Beads CLI (bd) is already installed
|
|
876
|
-
if command -v bd &>/dev/null; then
|
|
877
|
-
local bd_version
|
|
878
|
-
bd_version=$(bd --version 2>/dev/null | head -1 || echo "unknown")
|
|
879
|
-
print_success "Beads CLI (bd) already installed: $bd_version"
|
|
880
|
-
else
|
|
881
|
-
# Try to install via Homebrew first (macOS/Linux with Homebrew)
|
|
882
|
-
if command -v brew &>/dev/null; then
|
|
883
|
-
if run_with_spinner "Installing Beads via Homebrew" brew install steveyegge/beads/bd; then
|
|
884
|
-
: # Success message handled by spinner
|
|
885
|
-
else
|
|
886
|
-
print_warning "Homebrew tap installation failed, trying alternative..."
|
|
887
|
-
install_beads_binary || install_beads_go
|
|
888
|
-
fi
|
|
889
|
-
elif command -v go &>/dev/null; then
|
|
890
|
-
if run_with_spinner "Installing Beads via Go" go install github.com/steveyegge/beads/cmd/bd@latest; then
|
|
891
|
-
print_info "Ensure \$GOPATH/bin is in your PATH"
|
|
892
|
-
else
|
|
893
|
-
print_warning "Go installation failed, trying binary download..."
|
|
894
|
-
install_beads_binary
|
|
895
|
-
fi
|
|
896
|
-
else
|
|
897
|
-
# No brew, no Go -- try binary download first, then offer Homebrew install
|
|
898
|
-
if ! install_beads_binary; then
|
|
899
|
-
# Binary download failed -- offer to install Homebrew (Linux only)
|
|
900
|
-
if ensure_homebrew; then
|
|
901
|
-
# Homebrew now available, retry via tap
|
|
902
|
-
if run_with_spinner "Installing Beads via Homebrew" brew install steveyegge/beads/bd; then
|
|
903
|
-
: # Success
|
|
904
|
-
else
|
|
905
|
-
print_warning "Homebrew tap installation failed"
|
|
906
|
-
fi
|
|
907
|
-
else
|
|
908
|
-
print_warning "Beads CLI (bd) not installed"
|
|
909
|
-
echo ""
|
|
910
|
-
echo " Install options:"
|
|
911
|
-
echo " Binary download: https://github.com/steveyegge/beads/releases"
|
|
912
|
-
echo " macOS/Linux (Homebrew): brew install steveyegge/beads/bd"
|
|
913
|
-
echo " Go: go install github.com/steveyegge/beads/cmd/bd@latest"
|
|
914
|
-
echo ""
|
|
915
|
-
fi
|
|
916
|
-
fi
|
|
917
|
-
fi
|
|
918
|
-
fi
|
|
919
|
-
|
|
920
|
-
print_info "Beads provides task graph visualization for TODO.md and PLANS.md"
|
|
921
|
-
print_info "After installation, run: aidevops init beads"
|
|
922
|
-
|
|
923
|
-
# Offer to install optional Beads UI tools
|
|
924
|
-
setup_beads_ui
|
|
925
|
-
|
|
926
|
-
return 0
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
# _install_bv_tool: install the bv (beads_viewer) TUI tool.
|
|
930
|
-
# Returns 0 if installed, 1 if skipped or failed.
|
|
931
|
-
_install_bv_tool() {
|
|
932
|
-
setup_prompt install_viewer " Install bv (TUI with PageRank, critical path, graph analytics)? [Y/n]: " "Y"
|
|
933
|
-
if [[ ! "$install_viewer" =~ ^[Yy]?$ ]]; then
|
|
934
|
-
print_info "Install later:"
|
|
935
|
-
print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
|
|
936
|
-
print_info " Go: go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest"
|
|
937
|
-
return 1
|
|
938
|
-
fi
|
|
939
|
-
if command -v brew &>/dev/null; then
|
|
940
|
-
if run_with_spinner "Installing bv via Homebrew" brew install dicklesworthstone/tap/bv; then
|
|
941
|
-
print_info "Run: bv (in a beads-enabled project)"
|
|
942
|
-
return 0
|
|
943
|
-
else
|
|
944
|
-
print_warning "Homebrew install failed - try manually:"
|
|
945
|
-
print_info " brew install dicklesworthstone/tap/bv"
|
|
946
|
-
return 1
|
|
947
|
-
fi
|
|
948
|
-
elif command -v go &>/dev/null; then
|
|
949
|
-
if run_with_spinner "Installing bv via Go" go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest; then
|
|
950
|
-
print_info "Run: bv (in a beads-enabled project)"
|
|
951
|
-
return 0
|
|
952
|
-
else
|
|
953
|
-
print_warning "Go install failed"
|
|
954
|
-
return 1
|
|
955
|
-
fi
|
|
956
|
-
else
|
|
957
|
-
# Offer verified install script (download-then-execute, not piped)
|
|
958
|
-
setup_prompt use_script " Install bv via install script? [Y/n]: " "Y"
|
|
959
|
-
if [[ "$use_script" =~ ^[Yy]?$ ]]; then
|
|
960
|
-
if verified_install "bv (beads viewer)" "https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/main/install.sh"; then
|
|
961
|
-
print_info "Run: bv (in a beads-enabled project)"
|
|
962
|
-
return 0
|
|
963
|
-
else
|
|
964
|
-
print_warning "Install script failed - try manually:"
|
|
965
|
-
print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
|
|
966
|
-
return 1
|
|
967
|
-
fi
|
|
968
|
-
else
|
|
969
|
-
print_info "Install later:"
|
|
970
|
-
print_info " Homebrew: brew tap dicklesworthstone/tap && brew install dicklesworthstone/tap/bv"
|
|
971
|
-
print_info " Go: go install github.com/Dicklesworthstone/beads_viewer/cmd/bv@latest"
|
|
972
|
-
return 1
|
|
973
|
-
fi
|
|
974
|
-
fi
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
# _install_beads_node_tools: install beads-ui and bdui via npm.
|
|
978
|
-
# Echoes the count of tools installed to stdout.
|
|
979
|
-
# All informational output (spinner, status) goes to stderr so that
|
|
980
|
-
# command-substitution callers receive only the numeric count.
|
|
981
|
-
_install_beads_node_tools() {
|
|
982
|
-
local count=0
|
|
983
|
-
if ! command -v npm &>/dev/null; then
|
|
984
|
-
echo "$count"
|
|
985
|
-
return 0
|
|
986
|
-
fi
|
|
987
|
-
setup_prompt install_web " Install beads-ui (Web dashboard)? [Y/n]: " "Y"
|
|
988
|
-
if [[ "$install_web" =~ ^[Yy]?$ ]]; then
|
|
989
|
-
if run_with_spinner "Installing beads-ui" npm_global_install beads-ui 1>&2; then
|
|
990
|
-
print_info "Run: beads-ui" >&2
|
|
991
|
-
count=$((count + 1))
|
|
992
|
-
fi
|
|
993
|
-
fi
|
|
994
|
-
setup_prompt install_bdui " Install bdui (React/Ink TUI)? [Y/n]: " "Y"
|
|
995
|
-
if [[ "$install_bdui" =~ ^[Yy]?$ ]]; then
|
|
996
|
-
if run_with_spinner "Installing bdui" npm_global_install bdui 1>&2; then
|
|
997
|
-
print_info "Run: bdui" >&2
|
|
998
|
-
count=$((count + 1))
|
|
999
|
-
fi
|
|
1000
|
-
fi
|
|
1001
|
-
echo "$count"
|
|
1002
|
-
return 0
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
# _install_perles: install the perles BQL query language TUI via cargo.
|
|
1006
|
-
# Returns 0 if installed, 1 if skipped or unavailable.
|
|
1007
|
-
_install_perles() {
|
|
1008
|
-
if ! command -v cargo &>/dev/null; then
|
|
1009
|
-
return 1
|
|
1010
|
-
fi
|
|
1011
|
-
setup_prompt install_perles " Install perles (BQL query language TUI)? [Y/n]: " "Y"
|
|
1012
|
-
if [[ ! "$install_perles" =~ ^[Yy]?$ ]]; then
|
|
1013
|
-
return 1
|
|
1014
|
-
fi
|
|
1015
|
-
if run_with_spinner "Installing perles (Rust compile)" cargo install perles; then
|
|
1016
|
-
print_info "Run: perles"
|
|
1017
|
-
return 0
|
|
1018
|
-
fi
|
|
1019
|
-
return 1
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
setup_beads_ui() {
|
|
1023
|
-
echo ""
|
|
1024
|
-
print_info "Beads UI tools provide enhanced visualization:"
|
|
1025
|
-
echo " • bv (Go) - PageRank, critical path, graph analytics TUI"
|
|
1026
|
-
echo " • beads-ui (Node.js) - Web dashboard with live updates"
|
|
1027
|
-
echo " • bdui (Node.js) - React/Ink terminal UI"
|
|
1028
|
-
echo " • perles (Rust) - BQL query language TUI"
|
|
1029
|
-
echo ""
|
|
1030
|
-
|
|
1031
|
-
setup_prompt install_beads_ui "Install optional Beads UI tools? [Y/n]: " "Y"
|
|
1032
|
-
|
|
1033
|
-
if [[ ! "$install_beads_ui" =~ ^[Yy]?$ ]]; then
|
|
1034
|
-
print_info "Skipped Beads UI tools (can install later from beads.md docs)"
|
|
1035
|
-
return 0
|
|
1036
|
-
fi
|
|
1037
|
-
|
|
1038
|
-
local installed_count=0
|
|
1039
|
-
|
|
1040
|
-
# bv (beads_viewer) - Go TUI installed via Homebrew
|
|
1041
|
-
# https://github.com/Dicklesworthstone/beads_viewer
|
|
1042
|
-
if _install_bv_tool; then
|
|
1043
|
-
installed_count=$((installed_count + 1))
|
|
1044
|
-
fi
|
|
1045
|
-
|
|
1046
|
-
# beads-ui and bdui (Node.js)
|
|
1047
|
-
local node_count
|
|
1048
|
-
node_count=$(_install_beads_node_tools)
|
|
1049
|
-
installed_count=$((installed_count + node_count))
|
|
1050
|
-
|
|
1051
|
-
# perles (Rust)
|
|
1052
|
-
if _install_perles; then
|
|
1053
|
-
installed_count=$((installed_count + 1))
|
|
1054
|
-
fi
|
|
1055
|
-
|
|
1056
|
-
if [[ $installed_count -gt 0 ]]; then
|
|
1057
|
-
print_success "Installed $installed_count Beads UI tool(s)"
|
|
1058
|
-
else
|
|
1059
|
-
print_info "No Beads UI tools installed"
|
|
1060
|
-
fi
|
|
1061
|
-
|
|
1062
|
-
echo ""
|
|
1063
|
-
print_info "Beads UI documentation: ~/.aidevops/agents/tools/task-management/beads.md"
|
|
1064
|
-
|
|
1065
|
-
return 0
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
setup_safety_hooks() {
|
|
1069
|
-
print_info "Setting up Claude Code safety hooks..."
|
|
1070
|
-
|
|
1071
|
-
# Check Python is available
|
|
1072
|
-
if ! command -v python3 &>/dev/null; then
|
|
1073
|
-
print_warning "Python 3 not found - safety hooks require Python 3"
|
|
1074
|
-
return 0
|
|
1075
|
-
fi
|
|
1076
|
-
|
|
1077
|
-
local helper_script="$HOME/.aidevops/agents/scripts/install-hooks-helper.sh"
|
|
1078
|
-
if [[ ! -f "$helper_script" ]]; then
|
|
1079
|
-
# Fall back to repo copy (INSTALL_DIR set by setup.sh)
|
|
1080
|
-
helper_script="${INSTALL_DIR:-.}/.agents/scripts/install-hooks-helper.sh"
|
|
1081
|
-
fi
|
|
1082
|
-
|
|
1083
|
-
if [[ ! -f "$helper_script" ]]; then
|
|
1084
|
-
print_warning "install-hooks-helper.sh not found - skipping safety hooks"
|
|
1085
|
-
return 0
|
|
1086
|
-
fi
|
|
1087
|
-
|
|
1088
|
-
if bash "$helper_script" install; then
|
|
1089
|
-
print_success "Claude Code safety hooks installed"
|
|
1090
|
-
else
|
|
1091
|
-
print_warning "Safety hook installation encountered issues (non-critical)"
|
|
1092
|
-
fi
|
|
1093
|
-
return 0
|
|
1094
|
-
}
|