aidevops 2.172.18 → 2.172.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env bash
2
+ # Plugin functions: deploy_plugins, sanitize_plugin_namespace, generate_agent_skills, create_skill_symlinks, check_skill_updates, scan_imported_skills, multi-tenant
3
+ # Part of aidevops setup.sh modularization (t316.3)
4
+
5
+ # Shell safety baseline
6
+ set -Eeuo pipefail
7
+ IFS=$'\n\t'
8
+ # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
9
+ trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
10
+ shopt -s inherit_errexit 2>/dev/null || true
11
+
12
+ # Check if Python >= 3.10 is available (required by cisco-ai-skill-scanner).
13
+ # Returns 0 if a compatible Python is found (or installed via uv), 1 otherwise.
14
+ # On failure, prints a clear diagnostic with the version found and fix instructions.
15
+ check_python_for_skill_scanner() {
16
+ local required_major=3
17
+ local required_minor=10
18
+
19
+ # Helper: test whether a python binary meets the minimum version
20
+ _python_version_ok() {
21
+ local py_bin="$1"
22
+ local ver_output
23
+ ver_output=$("$py_bin" --version 2>/dev/null) || return 1
24
+ # "Python 3.11.5" -> extract major.minor
25
+ local major minor
26
+ major=$(echo "$ver_output" | sed -E 's/Python ([0-9]+)\..*/\1/')
27
+ minor=$(echo "$ver_output" | sed -E 's/Python [0-9]+\.([0-9]+).*/\1/')
28
+ if [[ "$major" -gt "$required_major" ]] ||
29
+ { [[ "$major" -eq "$required_major" ]] && [[ "$minor" -ge "$required_minor" ]]; }; then
30
+ return 0
31
+ fi
32
+ return 1
33
+ }
34
+
35
+ # 1. Check default python3
36
+ if command -v python3 &>/dev/null && _python_version_ok python3; then
37
+ return 0
38
+ fi
39
+
40
+ # 2. Check common versioned binaries (Homebrew, system)
41
+ local py_bin
42
+ for py_bin in python3.13 python3.12 python3.11 python3.10; do
43
+ if command -v "$py_bin" &>/dev/null && _python_version_ok "$py_bin"; then
44
+ return 0
45
+ fi
46
+ done
47
+
48
+ # 3. If uv is available, install Python 3.11 and retry
49
+ if command -v uv &>/dev/null; then
50
+ print_info "No Python >= 3.10 found. Installing Python 3.11 via uv..."
51
+ if uv python install 3.11; then
52
+ # uv installs to its managed path; check if python3.11 is now available
53
+ if command -v python3.11 &>/dev/null && _python_version_ok python3.11; then
54
+ print_success "Python 3.11 installed via uv"
55
+ return 0
56
+ fi
57
+ # uv may have installed it but not on PATH — check uv's python path
58
+ local uv_py
59
+ uv_py=$(uv python find 3.11 2>/dev/null) || true
60
+ if [[ -n "$uv_py" ]] && _python_version_ok "$uv_py"; then
61
+ print_success "Python 3.11 installed via uv (at $uv_py)"
62
+ return 0
63
+ fi
64
+ print_warning "uv installed Python 3.11 but it could not be found on PATH"
65
+ else
66
+ print_warning "uv python install 3.11 failed — see errors above"
67
+ fi
68
+ fi
69
+
70
+ # 4. No compatible Python found — emit clear error
71
+ local found_version="not installed"
72
+ if command -v python3 &>/dev/null; then
73
+ found_version=$(python3 --version 2>/dev/null || echo "unknown")
74
+ fi
75
+
76
+ print_warning "cisco-ai-skill-scanner requires Python >= 3.10, but found: $found_version"
77
+ print_info "Fix options:"
78
+ print_info " 1. brew install python@3.11 (macOS)"
79
+ print_info " 2. uv python install 3.11 (cross-platform, recommended)"
80
+ print_info " 3. sudo apt install python3.11 (Debian/Ubuntu)"
81
+ print_info "After installing, re-run: aidevops update"
82
+ return 1
83
+ }
84
+
85
+ sanitize_plugin_namespace() {
86
+ local ns="$1"
87
+ # Strip any path components, keep only the final directory name
88
+ # This prevents ../../../etc/passwd and /absolute/paths
89
+ ns=$(basename "$ns")
90
+ # Additional safety: reject if it starts with . or contains suspicious chars
91
+ if [[ "$ns" =~ ^\.|\.\.|[[:space:]]|[\\/] ]]; then
92
+ return 1
93
+ fi
94
+ # Reject empty result
95
+ if [[ -z "$ns" ]]; then
96
+ return 1
97
+ fi
98
+ echo "$ns"
99
+ return 0
100
+ }
101
+
102
+ deploy_plugins() {
103
+ local target_dir="$1"
104
+ local plugins_file="$2"
105
+
106
+ # Skip if no plugins.json or no jq
107
+ if [[ ! -f "$plugins_file" ]]; then
108
+ return 0
109
+ fi
110
+ if ! command -v jq &>/dev/null; then
111
+ print_warning "jq not found; skipping plugin deployment"
112
+ return 0
113
+ fi
114
+
115
+ local plugin_count
116
+ plugin_count=$(jq '.plugins | length' "$plugins_file" 2>/dev/null || echo "0")
117
+ if [[ "$plugin_count" -eq 0 ]]; then
118
+ return 0
119
+ fi
120
+
121
+ local enabled_count
122
+ enabled_count=$(jq '[.plugins[] | select(.enabled != false)] | length' "$plugins_file" 2>/dev/null || echo "0")
123
+ if [[ "$enabled_count" -eq 0 ]]; then
124
+ print_info "No enabled plugins to deploy ($plugin_count configured, all disabled)"
125
+ return 0
126
+ fi
127
+
128
+ # Remove directories for disabled plugins (cleanup)
129
+ local disabled_ns
130
+ local safe_ns
131
+ while IFS= read -r disabled_ns; do
132
+ [[ -z "$disabled_ns" ]] && continue
133
+ # Sanitize namespace to prevent path traversal
134
+ if ! safe_ns=$(sanitize_plugin_namespace "$disabled_ns"); then
135
+ print_warning " Skipping invalid plugin namespace: $disabled_ns"
136
+ continue
137
+ fi
138
+ if [[ -d "$target_dir/$safe_ns" ]]; then
139
+ rm -rf "${target_dir:?}/${safe_ns:?}"
140
+ print_info " Removed disabled plugin directory: $safe_ns"
141
+ fi
142
+ done < <(jq -r '.plugins[] | select(.enabled == false) | .namespace // empty' "$plugins_file" 2>/dev/null)
143
+
144
+ print_info "Deploying $enabled_count plugin(s)..."
145
+
146
+ local deployed=0
147
+ local failed=0
148
+ local skipped=0
149
+
150
+ # Process each enabled plugin
151
+ local safe_pns
152
+ while IFS=$'\t' read -r pname prepo pns pbranch; do
153
+ [[ -z "$pname" ]] && continue
154
+ pbranch="${pbranch:-main}"
155
+
156
+ # Sanitize namespace to prevent path traversal
157
+ if ! safe_pns=$(sanitize_plugin_namespace "$pns"); then
158
+ print_warning " Skipping plugin '$pname' with invalid namespace: $pns"
159
+ failed=$((failed + 1))
160
+ continue
161
+ fi
162
+
163
+ local clone_dir="$target_dir/$safe_pns"
164
+
165
+ if [[ -d "$clone_dir" ]]; then
166
+ # Plugin directory exists — skip re-clone during setup
167
+ # Users can force update via: aidevops plugin update [name]
168
+ skipped=$((skipped + 1))
169
+ continue
170
+ fi
171
+
172
+ # Clone plugin repo
173
+ print_info " Installing plugin '$pname' ($prepo)..."
174
+ if git clone --branch "$pbranch" --depth 1 "$prepo" "$clone_dir" 2>/dev/null; then
175
+ # Remove .git directory (tracked via plugins.json, not nested git)
176
+ rm -rf "$clone_dir/.git"
177
+ # Set permissions on any scripts
178
+ if [[ -d "$clone_dir/scripts" ]]; then
179
+ chmod +x "$clone_dir/scripts/"*.sh 2>/dev/null || true
180
+ fi
181
+ deployed=$((deployed + 1))
182
+ else
183
+ print_warning " Failed to install plugin '$pname' (network or auth issue)"
184
+ failed=$((failed + 1))
185
+ fi
186
+ done < <(jq -r '.plugins[] | select(.enabled != false) | [.name, .repo, .namespace, (.branch // "main")] | @tsv' "$plugins_file" 2>/dev/null)
187
+
188
+ # Summary
189
+ if [[ "$deployed" -gt 0 ]]; then
190
+ print_success "Deployed $deployed plugin(s)"
191
+ fi
192
+ if [[ "$skipped" -gt 0 ]]; then
193
+ print_info "$skipped plugin(s) already deployed (use 'aidevops plugin update' to refresh)"
194
+ fi
195
+ if [[ "$failed" -gt 0 ]]; then
196
+ print_warning "$failed plugin(s) failed to deploy (non-blocking)"
197
+ fi
198
+
199
+ return 0
200
+ }
201
+
202
+ generate_agent_skills() {
203
+ print_info "Generating Agent Skills SKILL.md files..."
204
+
205
+ local skills_script="$HOME/.aidevops/agents/scripts/generate-skills.sh"
206
+
207
+ if [[ -f "$skills_script" ]]; then
208
+ if bash "$skills_script" 2>/dev/null; then
209
+ print_success "Agent Skills SKILL.md files generated"
210
+ else
211
+ print_warning "Agent Skills generation encountered issues (non-critical)"
212
+ fi
213
+ else
214
+ print_warning "Agent Skills generator not found at $skills_script"
215
+ fi
216
+
217
+ return 0
218
+ }
219
+
220
+ create_skill_symlinks() {
221
+ print_info "Creating symlinks for imported skills..."
222
+
223
+ local skill_sources="$HOME/.aidevops/agents/configs/skill-sources.json"
224
+ local agents_dir="$HOME/.aidevops/agents"
225
+
226
+ # Skip if no skill-sources.json or jq not available
227
+ if [[ ! -f "$skill_sources" ]]; then
228
+ print_info "No imported skills found (skill-sources.json not present)"
229
+ return 0
230
+ fi
231
+
232
+ if ! command -v jq &>/dev/null; then
233
+ print_warning "jq not found - cannot create skill symlinks"
234
+ return 0
235
+ fi
236
+
237
+ # Check if there are any skills
238
+ local skill_count
239
+ skill_count=$(jq '.skills | length' "$skill_sources" 2>/dev/null || echo "0")
240
+
241
+ if [[ "$skill_count" -eq 0 ]]; then
242
+ print_info "No imported skills to symlink"
243
+ return 0
244
+ fi
245
+
246
+ # AI assistant skill directories
247
+ local skill_dirs=(
248
+ "$HOME/.config/opencode/skills"
249
+ "$HOME/.codex/skills"
250
+ "$HOME/.claude/skills"
251
+ "$HOME/.config/amp/tools"
252
+ )
253
+
254
+ # Create skill directories if they don't exist
255
+ for dir in "${skill_dirs[@]}"; do
256
+ mkdir -p "$dir" 2>/dev/null || true
257
+ done
258
+
259
+ local created_count=0
260
+
261
+ # Read each skill and create symlinks
262
+ while IFS= read -r skill_json; do
263
+ local name local_path
264
+ name=$(echo "$skill_json" | jq -r '.name')
265
+ local_path=$(echo "$skill_json" | jq -r '.local_path')
266
+
267
+ # Skip if path doesn't exist
268
+ local full_path="$agents_dir/${local_path#.agents/}"
269
+ if [[ ! -f "$full_path" ]]; then
270
+ print_warning "Skill file not found: $full_path"
271
+ continue
272
+ fi
273
+
274
+ # Create symlinks in each AI assistant directory
275
+ for skill_dir in "${skill_dirs[@]}"; do
276
+ local target_file
277
+
278
+ # Amp expects <name>.md directly, others expect <name>/SKILL.md
279
+ if [[ "$skill_dir" == *"/amp/tools" ]]; then
280
+ target_file="$skill_dir/${name}.md"
281
+ else
282
+ local target_dir="$skill_dir/$name"
283
+ target_file="$target_dir/SKILL.md"
284
+ # Create skill subdirectory
285
+ mkdir -p "$target_dir" 2>/dev/null || continue
286
+ fi
287
+
288
+ # Create symlink (remove existing first)
289
+ rm -f "$target_file" 2>/dev/null || true
290
+ if ln -sf "$full_path" "$target_file" 2>/dev/null; then
291
+ ((++created_count))
292
+ fi
293
+ done
294
+ done < <(jq -c '.skills[]' "$skill_sources" 2>/dev/null)
295
+
296
+ if [[ $created_count -gt 0 ]]; then
297
+ print_success "Created $created_count skill symlinks across AI assistants"
298
+ else
299
+ print_info "No skill symlinks created"
300
+ fi
301
+
302
+ return 0
303
+ }
304
+
305
+ check_skill_updates() {
306
+ print_info "Checking for skill updates..."
307
+
308
+ local skill_sources="$HOME/.aidevops/agents/configs/skill-sources.json"
309
+
310
+ # Skip if no skill-sources.json or required tools not available
311
+ if [[ ! -f "$skill_sources" ]]; then
312
+ print_info "No imported skills to check"
313
+ return 0
314
+ fi
315
+
316
+ if ! command -v jq &>/dev/null; then
317
+ print_warning "jq not found - cannot check skill updates"
318
+ return 0
319
+ fi
320
+
321
+ if ! command -v curl &>/dev/null; then
322
+ print_warning "curl not found - cannot check skill updates"
323
+ return 0
324
+ fi
325
+
326
+ local skill_count
327
+ skill_count=$(jq '.skills | length' "$skill_sources" 2>/dev/null || echo "0")
328
+
329
+ if [[ "$skill_count" -eq 0 ]]; then
330
+ print_info "No imported skills to check"
331
+ return 0
332
+ fi
333
+
334
+ local updates_available=0
335
+ local update_list=""
336
+
337
+ # Check each skill for updates
338
+ while IFS= read -r skill_json; do
339
+ local name upstream_url upstream_commit
340
+ name=$(echo "$skill_json" | jq -r '.name')
341
+ upstream_url=$(echo "$skill_json" | jq -r '.upstream_url')
342
+ upstream_commit=$(echo "$skill_json" | jq -r '.upstream_commit // empty')
343
+
344
+ # Skip skills without upstream URL or commit (e.g., context7 imports)
345
+ if [[ -z "$upstream_url" || "$upstream_url" == "null" ]]; then
346
+ continue
347
+ fi
348
+ if [[ -z "$upstream_commit" ]]; then
349
+ continue
350
+ fi
351
+
352
+ # Extract owner/repo from GitHub URL
353
+ local owner_repo
354
+ owner_repo=$(echo "$upstream_url" | sed -E 's|https://github.com/||; s|\.git$||; s|/tree/.*||')
355
+
356
+ if [[ -z "$owner_repo" || ! "$owner_repo" =~ / ]]; then
357
+ continue
358
+ fi
359
+
360
+ # Get latest commit from GitHub API (silent, with timeout)
361
+ local api_response latest_commit
362
+ api_response=$(curl -s --max-time 5 "https://api.github.com/repos/$owner_repo/commits?per_page=1" 2>/dev/null)
363
+
364
+ # Check if response is an array (success) or object (error like rate limit)
365
+ if echo "$api_response" | jq -e 'type == "array"' >/dev/null 2>&1; then
366
+ latest_commit=$(echo "$api_response" | jq -r '.[0].sha // empty')
367
+ else
368
+ # API returned error object, skip this skill
369
+ continue
370
+ fi
371
+
372
+ if [[ -n "$latest_commit" && "$latest_commit" != "$upstream_commit" ]]; then
373
+ ((++updates_available))
374
+ update_list="${update_list}\n - $name (${upstream_commit:0:7} → ${latest_commit:0:7})"
375
+ fi
376
+ done < <(jq -c '.skills[]' "$skill_sources" 2>/dev/null)
377
+
378
+ if [[ $updates_available -gt 0 ]]; then
379
+ print_warning "Skill updates available:$update_list"
380
+ print_info "Run: ~/.aidevops/agents/scripts/add-skill-helper.sh check-updates"
381
+ print_info "To update a skill: ~/.aidevops/agents/scripts/add-skill-helper.sh add <url> --force"
382
+ else
383
+ print_success "All imported skills are up to date"
384
+ fi
385
+
386
+ return 0
387
+ }
388
+
389
+ scan_imported_skills() {
390
+ print_info "Running security scan on imported skills..."
391
+
392
+ local security_helper="$HOME/.aidevops/agents/scripts/security-helper.sh"
393
+
394
+ if [[ ! -f "$security_helper" ]]; then
395
+ print_warning "security-helper.sh not found - skipping skill scan"
396
+ return 0
397
+ fi
398
+
399
+ # Install skill-scanner if not present
400
+ # Pre-check: cisco-ai-skill-scanner requires Python >= 3.10
401
+ # Fallback chain: uv -> pipx -> venv+symlink -> pip3 --user (legacy)
402
+ # PEP 668 (Ubuntu 24.04+) blocks pip3 --user, so we try isolated methods first
403
+ if ! command -v skill-scanner &>/dev/null; then
404
+ # Verify Python >= 3.10 is available (or install it via uv)
405
+ if ! check_python_for_skill_scanner; then
406
+ print_warning "Skipping Cisco Skill Scanner install (Python >= 3.10 required)"
407
+ return 0
408
+ fi
409
+
410
+ local installed=false
411
+
412
+ # 1. uv tool install (preferred - fast, isolated, manages its own Python)
413
+ if [[ "$installed" == "false" ]] && command -v uv &>/dev/null; then
414
+ print_info "Installing Cisco Skill Scanner via uv..."
415
+ if run_with_spinner "Installing cisco-ai-skill-scanner" uv tool install cisco-ai-skill-scanner; then
416
+ print_success "Cisco Skill Scanner installed via uv"
417
+ installed=true
418
+ fi
419
+ fi
420
+
421
+ # 2. pipx install (designed for isolated app installs)
422
+ if [[ "$installed" == "false" ]] && command -v pipx &>/dev/null; then
423
+ print_info "Installing Cisco Skill Scanner via pipx..."
424
+ if run_with_spinner "Installing cisco-ai-skill-scanner" pipx install cisco-ai-skill-scanner; then
425
+ print_success "Cisco Skill Scanner installed via pipx"
426
+ installed=true
427
+ fi
428
+ fi
429
+
430
+ # 3. venv + symlink (works on PEP 668 systems without uv/pipx)
431
+ if [[ "$installed" == "false" ]] && command -v python3 &>/dev/null; then
432
+ local venv_dir="$HOME/.aidevops/.agent-workspace/work/cisco-scanner-env"
433
+ local bin_dir="$HOME/.local/bin"
434
+ print_info "Installing Cisco Skill Scanner in isolated venv..."
435
+ if python3 -m venv "$venv_dir" 2>/dev/null &&
436
+ "$venv_dir/bin/pip" install cisco-ai-skill-scanner 2>/dev/null; then
437
+ mkdir -p "$bin_dir"
438
+ ln -sf "$venv_dir/bin/skill-scanner" "$bin_dir/skill-scanner"
439
+ print_success "Cisco Skill Scanner installed via venv ($venv_dir)"
440
+ installed=true
441
+ else
442
+ rm -rf "$venv_dir" 2>/dev/null || true
443
+ fi
444
+ fi
445
+
446
+ # 4. pip3 --user (legacy fallback, fails on PEP 668 systems)
447
+ if [[ "$installed" == "false" ]] && command -v pip3 &>/dev/null; then
448
+ print_info "Installing Cisco Skill Scanner via pip3 --user..."
449
+ if run_with_spinner "Installing cisco-ai-skill-scanner" pip3 install --user cisco-ai-skill-scanner 2>/dev/null; then
450
+ print_success "Cisco Skill Scanner installed via pip3"
451
+ installed=true
452
+ fi
453
+ fi
454
+
455
+ if [[ "$installed" == "false" ]]; then
456
+ print_warning "Failed to install Cisco Skill Scanner - skipping security scan"
457
+ print_info "Install manually with: uv tool install cisco-ai-skill-scanner"
458
+ print_info "Or: pipx install cisco-ai-skill-scanner"
459
+ return 0
460
+ fi
461
+ fi
462
+
463
+ if bash "$security_helper" skill-scan all 2>/dev/null; then
464
+ print_success "All imported skills passed security scan"
465
+ else
466
+ print_warning "Some imported skills have security findings - review with: aidevops skill scan"
467
+ fi
468
+
469
+ return 0
470
+ }
471
+
472
+ setup_multi_tenant_credentials() {
473
+ print_info "Multi-tenant credential storage..."
474
+
475
+ local credential_helper="$HOME/.aidevops/agents/scripts/credential-helper.sh"
476
+
477
+ if [[ ! -f "$credential_helper" ]]; then
478
+ # Try local script if deployed version not available yet
479
+ credential_helper=".agents/scripts/credential-helper.sh"
480
+ fi
481
+
482
+ if [[ ! -f "$credential_helper" ]]; then
483
+ print_warning "credential-helper.sh not found - skipping"
484
+ return 0
485
+ fi
486
+
487
+ # Check if already initialized
488
+ if [[ -d "$HOME/.config/aidevops/tenants" ]]; then
489
+ local tenant_count
490
+ tenant_count=$(find "$HOME/.config/aidevops/tenants" -maxdepth 1 -type d | wc -l)
491
+ # Subtract 1 for the tenants/ dir itself
492
+ tenant_count=$((tenant_count - 1))
493
+ print_success "Multi-tenant already initialized ($tenant_count tenant(s))"
494
+ bash "$credential_helper" status
495
+ return 0
496
+ fi
497
+
498
+ # Check if there are existing credentials to migrate
499
+ if [[ -f "$HOME/.config/aidevops/credentials.sh" ]]; then
500
+ local key_count
501
+ key_count=$(grep -c "^export " "$HOME/.config/aidevops/credentials.sh" 2>/dev/null || echo "0")
502
+ print_info "Found $key_count existing API keys in credentials.sh"
503
+ print_info "Multi-tenant enables managing separate credential sets for:"
504
+ echo " - Multiple clients (agency/freelance work)"
505
+ echo " - Multiple environments (production, staging)"
506
+ echo " - Multiple accounts (personal, work)"
507
+ echo ""
508
+ print_info "Your existing keys will be migrated to a 'default' tenant."
509
+ print_info "Everything continues to work as before - this is non-breaking."
510
+ echo ""
511
+
512
+ read -r -p "Enable multi-tenant credential storage? [Y/n]: " enable_mt
513
+ enable_mt=$(echo "$enable_mt" | tr '[:upper:]' '[:lower:]')
514
+
515
+ if [[ "$enable_mt" =~ ^[Yy]?$ || "$enable_mt" == "yes" ]]; then
516
+ bash "$credential_helper" init
517
+ print_success "Multi-tenant credential storage enabled"
518
+ echo ""
519
+ print_info "Quick start:"
520
+ echo " credential-helper.sh create client-name # Create a tenant"
521
+ echo " credential-helper.sh switch client-name # Switch active tenant"
522
+ echo " credential-helper.sh set KEY val --tenant X # Add key to tenant"
523
+ echo " credential-helper.sh status # Show current state"
524
+ else
525
+ print_info "Skipped. Enable later: credential-helper.sh init"
526
+ fi
527
+ else
528
+ print_info "No existing credentials found. Multi-tenant available when needed."
529
+ print_info "Enable later: credential-helper.sh init"
530
+ fi
531
+
532
+ return 0
533
+ }
534
+
535
+ check_tool_updates() {
536
+ print_info "Checking for tool updates..."
537
+
538
+ local tool_check_script="$HOME/.aidevops/agents/scripts/tool-version-check.sh"
539
+
540
+ if [[ ! -f "$tool_check_script" ]]; then
541
+ # Try local script if deployed version not available yet
542
+ tool_check_script=".agents/scripts/tool-version-check.sh"
543
+ fi
544
+
545
+ if [[ ! -f "$tool_check_script" ]]; then
546
+ print_warning "Tool version check script not found - skipping update check"
547
+ return 0
548
+ fi
549
+
550
+ # Run the check in quiet mode first to see if there are updates
551
+ # Capture both output and exit code
552
+ local outdated_output
553
+ local check_exit_code
554
+ outdated_output=$(bash "$tool_check_script" --quiet 2>&1) || check_exit_code=$?
555
+ check_exit_code=${check_exit_code:-0}
556
+
557
+ # If the script failed, warn and continue
558
+ if [[ $check_exit_code -ne 0 ]]; then
559
+ print_warning "Tool version check encountered an error (exit code: $check_exit_code)"
560
+ print_info "Run 'aidevops update-tools' manually to check for updates"
561
+ return 0
562
+ fi
563
+
564
+ if [[ -z "$outdated_output" ]]; then
565
+ print_success "All tools are up to date!"
566
+ return 0
567
+ fi
568
+
569
+ # Show what's outdated
570
+ echo ""
571
+ print_warning "Some tools have updates available:"
572
+ echo ""
573
+ bash "$tool_check_script" --quiet
574
+ echo ""
575
+
576
+ read -r -p "Update all outdated tools now? [Y/n]: " do_update
577
+
578
+ if [[ "$do_update" =~ ^[Yy]?$ || "$do_update" == "Y" ]]; then
579
+ print_info "Updating tools..."
580
+ bash "$tool_check_script" --update
581
+ print_success "Tool updates complete!"
582
+ else
583
+ print_info "Skipped tool updates"
584
+ print_info "Run 'aidevops update-tools' anytime to update tools"
585
+ fi
586
+
587
+ return 0
588
+ }