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,892 @@
1
+ #!/usr/bin/env bash
2
+ # Shell environment setup functions: oh-my-zsh, shell-compat, aliases, terminal-title
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
+ # Detect the shell currently executing this script (zsh, bash, or fallback)
13
+ detect_running_shell() {
14
+ if [[ -n "${ZSH_VERSION:-}" ]]; then
15
+ echo "zsh"
16
+ elif [[ -n "${BASH_VERSION:-}" ]]; then
17
+ echo "bash"
18
+ else
19
+ basename "${SHELL:-/bin/bash}"
20
+ fi
21
+ return 0
22
+ }
23
+
24
+ # Detect the user's default login shell from $SHELL
25
+ detect_default_shell() {
26
+ basename "${SHELL:-/bin/bash}"
27
+ return 0
28
+ }
29
+
30
+ # Usage: get_shell_rc "zsh" or get_shell_rc "bash"
31
+ get_shell_rc() {
32
+ local shell_name="$1"
33
+ case "$shell_name" in
34
+ zsh)
35
+ echo "$HOME/.zshrc"
36
+ ;;
37
+ bash)
38
+ if [[ "$(uname)" == "Darwin" ]]; then
39
+ echo "$HOME/.bash_profile"
40
+ else
41
+ echo "$HOME/.bashrc"
42
+ fi
43
+ ;;
44
+ fish)
45
+ echo "$HOME/.config/fish/config.fish"
46
+ ;;
47
+ ksh)
48
+ echo "$HOME/.kshrc"
49
+ ;;
50
+ *)
51
+ # Fallback: check common rc files
52
+ if [[ -f "$HOME/.zshrc" ]]; then
53
+ echo "$HOME/.zshrc"
54
+ elif [[ -f "$HOME/.bashrc" ]]; then
55
+ echo "$HOME/.bashrc"
56
+ elif [[ -f "$HOME/.bash_profile" ]]; then
57
+ echo "$HOME/.bash_profile"
58
+ else
59
+ echo ""
60
+ fi
61
+ ;;
62
+ esac
63
+ return 0
64
+ }
65
+
66
+ # Return all relevant shell rc file paths for the current platform
67
+ get_all_shell_rcs() {
68
+ local rcs=()
69
+
70
+ if [[ "$(uname)" == "Darwin" ]]; then
71
+ # macOS: always include zsh (default since Catalina) and bash_profile
72
+ [[ -f "$HOME/.zshrc" ]] && rcs+=("$HOME/.zshrc")
73
+ [[ -f "$HOME/.bash_profile" ]] && rcs+=("$HOME/.bash_profile")
74
+ # If neither exists, create .zshrc (macOS default)
75
+ if [[ ${#rcs[@]} -eq 0 ]]; then
76
+ touch "$HOME/.zshrc"
77
+ rcs+=("$HOME/.zshrc")
78
+ fi
79
+ else
80
+ # Linux: use the default shell's rc file
81
+ local default_shell
82
+ default_shell=$(detect_default_shell)
83
+ local rc
84
+ rc=$(get_shell_rc "$default_shell")
85
+ if [[ -n "$rc" ]]; then
86
+ rcs+=("$rc")
87
+ fi
88
+ fi
89
+
90
+ printf '%s\n' "${rcs[@]}"
91
+ return 0
92
+ }
93
+
94
+ # Offer to install Oh My Zsh if zsh is the default shell and OMZ is not present
95
+ setup_oh_my_zsh() {
96
+ # Only relevant if zsh is available
97
+ if ! command -v zsh >/dev/null 2>&1; then
98
+ print_info "zsh not found - skipping Oh My Zsh setup"
99
+ return 0
100
+ fi
101
+
102
+ # Check if Oh My Zsh is already installed
103
+ if [[ -d "$HOME/.oh-my-zsh" ]]; then
104
+ print_success "Oh My Zsh already installed"
105
+ return 0
106
+ fi
107
+
108
+ local default_shell
109
+ default_shell=$(detect_default_shell)
110
+
111
+ # Only offer if zsh is the default shell (or on macOS where it's the system default)
112
+ if [[ "$default_shell" != "zsh" && "$(uname)" != "Darwin" ]]; then
113
+ print_info "Default shell is $default_shell (not zsh) - skipping Oh My Zsh"
114
+ return 0
115
+ fi
116
+
117
+ print_info "Oh My Zsh enhances zsh with themes, plugins, and completions"
118
+ echo " Many tools installed later (git, fd, brew) benefit from Oh My Zsh plugins."
119
+ echo " This is optional - plain zsh works fine without it."
120
+ echo ""
121
+
122
+ read -r -p "Install Oh My Zsh? [y/N]: " install_omz
123
+
124
+ if [[ "$install_omz" =~ ^[Yy]$ ]]; then
125
+ print_info "Installing Oh My Zsh..."
126
+ # Use verified download + --unattended to avoid changing the shell or starting zsh
127
+ # shellcheck disable=SC2034 # Read by verified_install() in setup.sh
128
+ VERIFIED_INSTALL_SHELL="sh"
129
+ if verified_install "Oh My Zsh" "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" --unattended; then
130
+ print_success "Oh My Zsh installed"
131
+
132
+ # Ensure .zshrc exists (Oh My Zsh creates it, but verify)
133
+ if [[ ! -f "$HOME/.zshrc" ]]; then
134
+ print_warning ".zshrc not created - Oh My Zsh may not have installed correctly"
135
+ fi
136
+
137
+ # If the user's default shell isn't zsh, offer to change it
138
+ if [[ "$default_shell" != "zsh" ]]; then
139
+ echo ""
140
+ read -r -p "Change default shell to zsh? [y/N]: " change_shell
141
+ if [[ "$change_shell" =~ ^[Yy]$ ]]; then
142
+ if chsh -s "$(command -v zsh)"; then
143
+ print_success "Default shell changed to zsh"
144
+ print_info "Restart your terminal for the change to take effect"
145
+ else
146
+ print_warning "Failed to change shell - run manually: chsh -s $(command -v zsh)"
147
+ fi
148
+ fi
149
+ fi
150
+ else
151
+ print_warning "Oh My Zsh installation failed"
152
+ print_info "Install manually: curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -o /tmp/omz-install.sh && sh /tmp/omz-install.sh"
153
+ fi
154
+ else
155
+ print_info "Skipped Oh My Zsh installation"
156
+ print_info "Install later: curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -o /tmp/omz-install.sh && sh /tmp/omz-install.sh"
157
+ fi
158
+
159
+ return 0
160
+ }
161
+
162
+ # Extract portable customizations from bash configs into a shared profile for cross-shell use
163
+ setup_shell_compatibility() {
164
+ print_info "Setting up cross-shell compatibility..."
165
+
166
+ local shared_profile="$HOME/.shell_common"
167
+ local zsh_rc="$HOME/.zshrc"
168
+
169
+ # If shared profile already exists, we've already set this up
170
+ if [[ -f "$shared_profile" ]]; then
171
+ print_success "Cross-shell compatibility already configured ($shared_profile)"
172
+ return 0
173
+ fi
174
+
175
+ # Need both bash and zsh to be relevant
176
+ if ! command -v zsh >/dev/null 2>&1; then
177
+ print_info "zsh not installed - cross-shell setup not needed"
178
+ return 0
179
+ fi
180
+ if ! command -v bash >/dev/null 2>&1; then
181
+ print_info "bash not installed - cross-shell setup not needed"
182
+ return 0
183
+ fi
184
+
185
+ # Collect all bash config files that exist
186
+ # macOS: .bash_profile (login) + .bashrc (interactive, often sourced by .bash_profile)
187
+ # Linux: .bashrc (primary) + .bash_profile (login, often sources .bashrc)
188
+ # We check all of them on both platforms since tools write to either
189
+ local -a bash_files=()
190
+ [[ -f "$HOME/.bash_profile" ]] && bash_files+=("$HOME/.bash_profile")
191
+ [[ -f "$HOME/.bashrc" ]] && bash_files+=("$HOME/.bashrc")
192
+ [[ -f "$HOME/.profile" ]] && bash_files+=("$HOME/.profile")
193
+
194
+ if [[ ${#bash_files[@]} -eq 0 ]]; then
195
+ print_info "No bash config files found - skipping cross-shell setup"
196
+ return 0
197
+ fi
198
+
199
+ if [[ ! -f "$zsh_rc" ]]; then
200
+ print_info "No .zshrc found - skipping cross-shell setup"
201
+ return 0
202
+ fi
203
+
204
+ # Count customizations across all bash config files
205
+ local total_exports=0
206
+ local total_aliases=0
207
+ local total_paths=0
208
+
209
+ for src_file in "${bash_files[@]}"; do
210
+ local n
211
+ # grep -c exits 1 on no match; || : prevents ERR trap noise
212
+ # File existence already verified when building bash_files array
213
+ n=$(grep -cE '^\s*export\s+[A-Z]' "$src_file" || :)
214
+ total_exports=$((total_exports + ${n:-0}))
215
+ n=$(grep -cE '^\s*alias\s+' "$src_file" || :)
216
+ total_aliases=$((total_aliases + ${n:-0}))
217
+ n=$(grep -cE 'PATH.*=' "$src_file" || :)
218
+ total_paths=$((total_paths + ${n:-0}))
219
+ done
220
+
221
+ if [[ $total_exports -eq 0 && $total_aliases -eq 0 && $total_paths -eq 0 ]]; then
222
+ print_info "No bash customizations detected - skipping cross-shell setup"
223
+ return 0
224
+ fi
225
+
226
+ print_info "Detected bash customizations across ${#bash_files[@]} file(s):"
227
+ echo " Exports: $total_exports, Aliases: $total_aliases, PATH entries: $total_paths"
228
+ echo ""
229
+ print_info "Best practice: create a shared profile (~/.shell_common) sourced by"
230
+ print_info "both bash and zsh, so your customizations work in either shell."
231
+ echo ""
232
+
233
+ local setup_compat="Y"
234
+ if [[ "$NON_INTERACTIVE" != "true" ]]; then
235
+ read -r -p "Create shared shell profile for cross-shell compatibility? [Y/n]: " setup_compat
236
+ fi
237
+
238
+ if [[ ! "$setup_compat" =~ ^[Yy]?$ ]]; then
239
+ print_info "Skipped cross-shell compatibility setup"
240
+ print_info "Set up later by creating ~/.shell_common and sourcing it from both shells"
241
+ return 0
242
+ fi
243
+
244
+ # Extract portable customizations from bash config into shared profile
245
+ # We extract: exports, PATH modifications, aliases, eval statements, source commands
246
+ # We skip: bash-specific syntax (shopt, PROMPT_COMMAND, PS1, completion, bind, etc.)
247
+ # We deduplicate lines that appear in multiple files (e.g. .bash_profile sources .bashrc)
248
+ print_info "Creating shared profile: $shared_profile"
249
+
250
+ {
251
+ echo "# Shared shell profile - sourced by both bash and zsh"
252
+ echo "# Created by aidevops setup to preserve customizations across shell switches"
253
+ echo "# Edit this file for settings you want in BOTH bash and zsh"
254
+ echo "# Shell-specific settings go in ~/.bashrc or ~/.zshrc"
255
+ echo ""
256
+ } >"$shared_profile"
257
+
258
+ # Track lines we've already written to avoid duplicates
259
+ # (common on Linux where .bash_profile sources .bashrc)
260
+ local -a seen_lines=()
261
+ local extracted=0
262
+
263
+ for src_file in "${bash_files[@]}"; do
264
+ local src_basename
265
+ src_basename=$(basename "$src_file")
266
+ local added_header=false
267
+
268
+ while IFS= read -r line || [[ -n "$line" ]]; do
269
+ # Skip empty lines
270
+ [[ -z "$line" ]] && continue
271
+ # Skip pure comment lines
272
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
273
+
274
+ # Skip bash-specific settings that don't work in zsh
275
+ case "$line" in
276
+ *shopt*) continue ;;
277
+ *PROMPT_COMMAND*) continue ;;
278
+ *PS1=*) continue ;;
279
+ *PS2=*) continue ;;
280
+ *bash_completion*) continue ;;
281
+ *"complete "*) continue ;;
282
+ *"bind "*) continue ;;
283
+ *HISTCONTROL*) continue ;;
284
+ *HISTFILESIZE*) continue ;;
285
+ *HISTSIZE*) continue ;;
286
+ *"source /etc/bash"*) continue ;;
287
+ *". /etc/bash"*) continue ;;
288
+ *"source /etc/profile"*) continue ;;
289
+ *". /etc/profile"*) continue ;;
290
+ # Skip lines that source .bashrc from .bash_profile (circular)
291
+ *".bashrc"*) continue ;;
292
+ # Skip lines that source .shell_common (we'll add this ourselves)
293
+ *"shell_common"*) continue ;;
294
+ esac
295
+
296
+ # Match portable lines: exports, aliases, PATH, eval, source/dot-source
297
+ local is_portable=false
298
+ case "$line" in
299
+ export\ [A-Z]* | export\ PATH*) is_portable=true ;;
300
+ alias\ *) is_portable=true ;;
301
+ eval\ *) is_portable=true ;;
302
+ *PATH=*) is_portable=true ;;
303
+ esac
304
+ # Also match 'source' and '. ' commands (tool integrations like nvm, rvm, pyenv)
305
+ if [[ "$is_portable" == "false" ]]; then
306
+ case "$line" in
307
+ source\ * | .\ /* | .\ \$* | .\ \~*) is_portable=true ;;
308
+ esac
309
+ fi
310
+
311
+ if [[ "$is_portable" == "true" ]]; then
312
+ # Deduplicate: skip if we've already seen this exact line
313
+ local is_dup=false
314
+ local seen
315
+ for seen in "${seen_lines[@]}"; do
316
+ if [[ "$seen" == "$line" ]]; then
317
+ is_dup=true
318
+ break
319
+ fi
320
+ done
321
+ if [[ "$is_dup" == "true" ]]; then
322
+ continue
323
+ fi
324
+
325
+ if [[ "$added_header" == "false" ]]; then
326
+ echo "" >>"$shared_profile"
327
+ echo "# From $src_basename" >>"$shared_profile"
328
+ added_header=true
329
+ fi
330
+ echo "$line" >>"$shared_profile"
331
+ seen_lines+=("$line")
332
+ ((++extracted))
333
+ fi
334
+ done <"$src_file"
335
+ done
336
+
337
+ if [[ $extracted -eq 0 ]]; then
338
+ rm -f "$shared_profile"
339
+ print_info "No portable customizations found to extract"
340
+ return 0
341
+ fi
342
+
343
+ chmod 644 "$shared_profile"
344
+ print_success "Extracted $extracted unique customization(s) to $shared_profile"
345
+
346
+ # Add sourcing to .zshrc if not already present (existence verified above)
347
+ if ! grep -q 'shell_common' "$zsh_rc"; then
348
+ {
349
+ echo ""
350
+ echo "# Cross-shell compatibility (added by aidevops setup)"
351
+ echo "# Sources shared profile so bash customizations work in zsh too"
352
+ # shellcheck disable=SC2016
353
+ echo '[ -f "$HOME/.shell_common" ] && . "$HOME/.shell_common"'
354
+ } >>"$zsh_rc"
355
+ print_success "Added shared profile sourcing to .zshrc"
356
+ fi
357
+
358
+ # Add sourcing to bash config files if not already present
359
+ # File existence already verified when building bash_files array
360
+ for src_file in "${bash_files[@]}"; do
361
+ if ! grep -q 'shell_common' "$src_file"; then
362
+ {
363
+ echo ""
364
+ echo "# Cross-shell compatibility (added by aidevops setup)"
365
+ echo "# Shared profile - edit ~/.shell_common for settings in both shells"
366
+ # shellcheck disable=SC2016
367
+ echo '[ -f "$HOME/.shell_common" ] && . "$HOME/.shell_common"'
368
+ } >>"$src_file"
369
+ print_success "Added shared profile sourcing to $(basename "$src_file")"
370
+ fi
371
+ done
372
+
373
+ echo ""
374
+ print_success "Cross-shell compatibility configured"
375
+ print_info "Your customizations are now in: $shared_profile"
376
+ print_info "Both bash and zsh will source this file automatically."
377
+ print_info "Edit ~/.shell_common for settings you want in both shells."
378
+ print_info "Use ~/.bashrc or ~/.zshrc for shell-specific settings only."
379
+
380
+ return 0
381
+ }
382
+
383
+ # Check for optional dependencies (sshpass) and offer to install them
384
+ check_optional_deps() {
385
+ print_info "Checking optional dependencies..."
386
+
387
+ local missing_optional=()
388
+
389
+ if ! command -v sshpass >/dev/null 2>&1; then
390
+ missing_optional+=("sshpass")
391
+ else
392
+ print_success "sshpass found"
393
+ fi
394
+
395
+ if [[ ${#missing_optional[@]} -gt 0 ]]; then
396
+ print_warning "Missing optional dependencies: ${missing_optional[*]}"
397
+ echo " sshpass - needed for password-based SSH (like Hostinger)"
398
+
399
+ local pkg_manager
400
+ pkg_manager=$(detect_package_manager)
401
+
402
+ if [[ "$pkg_manager" != "unknown" ]]; then
403
+ read -r -p "Install optional dependencies using $pkg_manager? [Y/n]: " install_optional
404
+
405
+ if [[ "$install_optional" =~ ^[Yy]?$ ]]; then
406
+ print_info "Installing ${missing_optional[*]}..."
407
+ if install_packages "$pkg_manager" "${missing_optional[@]}"; then
408
+ print_success "Optional dependencies installed"
409
+ else
410
+ print_warning "Failed to install optional dependencies (non-critical)"
411
+ fi
412
+ else
413
+ print_info "Skipped optional dependencies"
414
+ fi
415
+ fi
416
+ fi
417
+ return 0
418
+ }
419
+
420
+ # Add ~/.local/bin to PATH in all shell rc files for the aidevops CLI
421
+ add_local_bin_to_path() {
422
+ # shellcheck disable=SC2016 # path_line is written to rc files; must expand at shell startup, not now
423
+ local path_line='export PATH="$HOME/.local/bin:$PATH"'
424
+ local added_to=""
425
+ local already_in=""
426
+
427
+ local rc_file
428
+ while IFS= read -r rc_file; do
429
+ [[ -z "$rc_file" ]] && continue
430
+
431
+ # Create the rc file if it doesn't exist (ensure parent dir exists for fish etc.)
432
+ if [[ ! -f "$rc_file" ]]; then
433
+ mkdir -p "$(dirname "$rc_file")"
434
+ touch "$rc_file"
435
+ fi
436
+
437
+ # Check if already added (file created above if it didn't exist)
438
+ if grep -q '\.local/bin' "$rc_file"; then
439
+ already_in="${already_in:+$already_in, }$rc_file"
440
+ continue
441
+ fi
442
+
443
+ # Add to shell config
444
+ {
445
+ echo ""
446
+ echo "# Added by aidevops setup"
447
+ echo "$path_line"
448
+ } >>"$rc_file"
449
+ added_to="${added_to:+$added_to, }$rc_file"
450
+ done < <(get_all_shell_rcs)
451
+
452
+ if [[ -n "$added_to" ]]; then
453
+ print_success "Added $HOME/.local/bin to PATH in: $added_to"
454
+ print_info "Restart your terminal to use 'aidevops' command"
455
+ fi
456
+
457
+ if [[ -n "$already_in" ]]; then
458
+ print_info "$HOME/.local/bin already in PATH in: $already_in"
459
+ fi
460
+
461
+ if [[ -z "$added_to" && -z "$already_in" ]]; then
462
+ print_warning "Could not detect shell config file"
463
+ print_info "Add this to your shell config: $path_line"
464
+ fi
465
+
466
+ # Also export for current session
467
+ export PATH="$HOME/.local/bin:$PATH"
468
+
469
+ return 0
470
+ }
471
+
472
+ # GH#2915, GH#2993: Ensure all processes use the safe ShellCheck wrapper.
473
+ #
474
+ # The bash language server hardcodes --external-sources in every ShellCheck
475
+ # invocation, causing exponential memory growth (9+ GB) when source chains
476
+ # span 463+ scripts. The wrapper strips --external-sources and enforces a
477
+ # background RSS watchdog (ulimit -v is broken on macOS ARM — EINVAL).
478
+ #
479
+ # GH#2915 set SHELLCHECK_PATH env var, but bash-language-server ignores it —
480
+ # it resolves `shellcheck` via PATH lookup, finding /opt/homebrew/bin/shellcheck
481
+ # directly. GH#2993 fixes this by placing a shim on PATH ahead of the real binary.
482
+ #
483
+ # Four layers ensure all processes use the wrapper:
484
+ # 0. PATH shim — ~/.aidevops/bin/shellcheck symlink + PATH prepend (GH#2993)
485
+ # 1. launchctl setenv (macOS) — GUI-launched apps (current boot only)
486
+ # 2. .zshenv — ALL zsh processes including non-interactive (persists)
487
+ # 3. Shell rc files (.zshrc, .bash_profile) — interactive terminals
488
+ #
489
+ # Layer 0 is the primary fix: bash-language-server does a PATH lookup for
490
+ # "shellcheck". By placing ~/.aidevops/bin first on PATH with a symlink to
491
+ # the wrapper, the language server finds the wrapper instead of the real binary.
492
+ # Layers 1-3 are retained for tools that honour SHELLCHECK_PATH.
493
+ #
494
+ # CRITICAL: ~/.aidevops/bin MUST be at the START of PATH, not the end.
495
+ # If it appears after /opt/homebrew/bin, the real shellcheck is found first
496
+ # and the wrapper is bypassed entirely. The launchctl setenv always prepends,
497
+ # and the case-guard in shell rc files ensures it stays first.
498
+ setup_shellcheck_wrapper() {
499
+ local wrapper_path="$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"
500
+
501
+ # Verify the wrapper exists and is executable
502
+ if [[ ! -x "$wrapper_path" ]]; then
503
+ if [[ -f "$wrapper_path" ]]; then
504
+ chmod +x "$wrapper_path"
505
+ else
506
+ print_warning "ShellCheck wrapper not found at $wrapper_path (will be available after deploy)"
507
+ return 0
508
+ fi
509
+ fi
510
+
511
+ # Verify the wrapper actually works (can find real shellcheck)
512
+ if ! "$wrapper_path" --version >/dev/null 2>&1; then
513
+ print_warning "ShellCheck wrapper cannot find real shellcheck binary — skipping"
514
+ return 0
515
+ fi
516
+
517
+ local env_line
518
+ # shellcheck disable=SC2016 # env_line is written to rc files; must expand at shell startup
519
+ env_line='export SHELLCHECK_PATH="$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"'
520
+ # shellcheck disable=SC2016 # path_line is written to rc files; must expand at shell startup
521
+ # Sanitize-and-prepend: strip any existing occurrence of the shim dir from PATH
522
+ # (it may be at the END from a previous setup run), then prepend it. This ensures
523
+ # the shim is always first, even on machines upgrading from the old append form.
524
+ # The ${PATH:+:$PATH} guard handles the empty-PATH edge case without a trailing colon.
525
+ local path_line='_aidevops_shim="$HOME/.aidevops/bin"; PATH="$(printf '\''%s'\'' "$PATH" | tr '\'':'\'' '\''\n'\'' | grep -Fxv -- "$_aidevops_shim" | paste -sd: -)"; export PATH="$_aidevops_shim${PATH:+:$PATH}"; unset _aidevops_shim'
526
+ # Fish shell uses different syntax (set -gx instead of export)
527
+ # shellcheck disable=SC2016 # fish lines are written to config.fish; must expand at shell startup
528
+ local env_line_fish='set -gx SHELLCHECK_PATH "$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"'
529
+ # shellcheck disable=SC2016 # fish path line: strip existing, then prepend
530
+ local path_line_fish='set -l _aidevops_shim "$HOME/.aidevops/bin"; set -l _aidevops_rest (string match -v -- "$_aidevops_shim" $PATH); set -gx PATH $_aidevops_shim $_aidevops_rest'
531
+ local added_to=""
532
+ local already_in=""
533
+
534
+ # Layer 0: PATH shim (GH#2993)
535
+ # Create ~/.aidevops/bin/shellcheck as a symlink to the wrapper.
536
+ # This is the primary fix: bash-language-server resolves `shellcheck` via
537
+ # PATH, so the symlink must appear on PATH before /opt/homebrew/bin.
538
+ local shim_dir="$HOME/.aidevops/bin"
539
+ local shim_path="$shim_dir/shellcheck"
540
+ mkdir -p "$shim_dir"
541
+
542
+ # Create or update the symlink
543
+ local wrapper_realpath
544
+ wrapper_realpath="$(realpath "$wrapper_path" 2>/dev/null || readlink -f "$wrapper_path" 2>/dev/null || echo "$wrapper_path")"
545
+ if [[ -L "$shim_path" ]]; then
546
+ local current_target
547
+ current_target="$(realpath "$shim_path" 2>/dev/null || readlink -f "$shim_path" 2>/dev/null || echo "")"
548
+ if [[ "$current_target" != "$wrapper_realpath" ]]; then
549
+ ln -sf "$wrapper_path" "$shim_path"
550
+ print_info "Updated shellcheck shim symlink: $shim_path → $wrapper_path"
551
+ fi
552
+ elif [[ -e "$shim_path" ]]; then
553
+ # Regular file exists — back up and replace with symlink
554
+ mv "$shim_path" "${shim_path}.bak.$(date +%Y%m%d_%H%M%S)"
555
+ ln -sf "$wrapper_path" "$shim_path"
556
+ print_info "Replaced shellcheck shim with symlink: $shim_path → $wrapper_path"
557
+ else
558
+ ln -sf "$wrapper_path" "$shim_path"
559
+ print_success "Created shellcheck shim: $shim_path → $wrapper_path"
560
+ fi
561
+
562
+ # Layer 1: launchctl setenv (macOS) — affects all GUI-launched processes
563
+ # Set both SHELLCHECK_PATH (for tools that honour it) and PATH (for tools
564
+ # that resolve shellcheck via PATH lookup, like bash-language-server).
565
+ # Note: 2>/dev/null on launchctl is intentional — launchctl may not be
566
+ # available in non-GUI contexts (SSH, containers). Unlike grep where we
567
+ # want errors visible, launchctl failure is a non-fatal fallback.
568
+ #
569
+ # CRITICAL: Always prepend shim_dir even if it's already in PATH — it may
570
+ # be at the END (e.g., appended by a previous setup run), which means the
571
+ # real shellcheck at /opt/homebrew/bin is found first. We strip any existing
572
+ # occurrence and prepend to guarantee first position.
573
+ if [[ "$PLATFORM_MACOS" == "true" ]]; then
574
+ if launchctl setenv SHELLCHECK_PATH "$wrapper_path" 2>/dev/null; then
575
+ print_info "Set SHELLCHECK_PATH via launchctl (GUI processes)"
576
+ fi
577
+ # Build a clean PATH with shim_dir at the front, removing any existing
578
+ # occurrence to prevent duplicates while ensuring first position.
579
+ # Handle the empty-PATH edge case to avoid a trailing colon (which
580
+ # resolves to "." and is a PATH injection vector).
581
+ local clean_path
582
+ clean_path=$(printf '%s' "$PATH" | tr ':' '\n' | grep -Fxv "$shim_dir" | tr '\n' ':' | sed 's/:$//')
583
+ local new_path
584
+ if [[ -n "$clean_path" ]]; then
585
+ new_path="${shim_dir}:${clean_path}"
586
+ else
587
+ new_path="${shim_dir}"
588
+ fi
589
+ if launchctl setenv PATH "$new_path" 2>/dev/null; then
590
+ print_info "Prepended $shim_dir to PATH via launchctl (GUI processes)"
591
+ fi
592
+ fi
593
+
594
+ # Layer 2: .zshenv — affects ALL zsh processes (interactive AND non-interactive)
595
+ # This is critical because opencode spawns bash-language-server as a
596
+ # non-interactive child process. .zshrc is NOT sourced for non-interactive
597
+ # shells, so SHELLCHECK_PATH set only in .zshrc is invisible to the LSP.
598
+ # GH#2993: Also prepend ~/.aidevops/bin to PATH here so the shim is found.
599
+ local zshenv="$HOME/.zshenv"
600
+ if [[ -f "$zshenv" ]] || command -v zsh >/dev/null 2>&1; then
601
+ touch "$zshenv"
602
+
603
+ # SHELLCHECK_PATH env var (for tools that honour it)
604
+ if grep -q 'SHELLCHECK_PATH' "$zshenv"; then
605
+ already_in="${already_in:+$already_in, }$zshenv"
606
+ else
607
+ {
608
+ echo ""
609
+ echo "# Added by aidevops setup (GH#2915: prevent ShellCheck memory explosion)"
610
+ echo "$env_line"
611
+ } >>"$zshenv"
612
+ added_to="${added_to:+$added_to, }$zshenv"
613
+ fi
614
+
615
+ # PATH prepend for ~/.aidevops/bin (GH#2993: shim must be on PATH)
616
+ # Remove stale old-form entries (case guard that only checked presence,
617
+ # not position — left the shim at the end of PATH on upgrades)
618
+ # shellcheck disable=SC2016 # Matching literal $PATH text in rc files, not expanding
619
+ if grep -q 'case ":$PATH:" in.*\.aidevops/bin' "$zshenv"; then
620
+ # Remove the old case-guard line (sed is appropriate here — targeted single-line removal)
621
+ # shellcheck disable=SC2016
622
+ sed -i.bak '/case ":$PATH:" in.*\.aidevops\/bin/d' "$zshenv"
623
+ rm -f "${zshenv}.bak"
624
+ fi
625
+ # Use exact-line match for the new sanitize-and-prepend form
626
+ if ! grep -Fq '_aidevops_shim' "$zshenv"; then
627
+ {
628
+ echo ""
629
+ echo "# Added by aidevops setup (GH#2993: shellcheck shim on PATH)"
630
+ echo "$path_line"
631
+ } >>"$zshenv"
632
+ print_success "Prepended $shim_dir to PATH in .zshenv"
633
+ else
634
+ print_info "$shim_dir already on PATH in .zshenv"
635
+ fi
636
+ fi
637
+
638
+ # Layer 3: Shell rc files — affects interactive terminal sessions
639
+ local rc_file
640
+ while IFS= read -r rc_file; do
641
+ [[ -z "$rc_file" ]] && continue
642
+
643
+ if [[ ! -f "$rc_file" ]]; then
644
+ mkdir -p "$(dirname "$rc_file")"
645
+ touch "$rc_file"
646
+ fi
647
+
648
+ # Detect fish config — uses set -gx syntax, not export
649
+ local is_fish_rc=false
650
+ if [[ "$rc_file" == *"/fish/config.fish" ]]; then
651
+ is_fish_rc=true
652
+ fi
653
+
654
+ # Select the correct syntax for this shell
655
+ local rc_env_line="$env_line"
656
+ local rc_path_line="$path_line"
657
+ if [[ "$is_fish_rc" == "true" ]]; then
658
+ rc_env_line="$env_line_fish"
659
+ rc_path_line="$path_line_fish"
660
+ fi
661
+
662
+ # SHELLCHECK_PATH env var
663
+ if grep -q 'SHELLCHECK_PATH' "$rc_file"; then
664
+ already_in="${already_in:+$already_in, }$rc_file"
665
+ else
666
+ {
667
+ echo ""
668
+ echo "# Added by aidevops setup (GH#2915: prevent ShellCheck memory explosion)"
669
+ echo "$rc_env_line"
670
+ } >>"$rc_file"
671
+ added_to="${added_to:+$added_to, }$rc_file"
672
+ fi
673
+
674
+ # PATH prepend for ~/.aidevops/bin (GH#2993)
675
+ # Remove stale old-form entries (case guard that only checked presence,
676
+ # not position — left the shim at the end of PATH on upgrades)
677
+ # shellcheck disable=SC2016 # Matching literal $PATH text in rc files, not expanding
678
+ if grep -q 'case ":$PATH:" in.*\.aidevops/bin' "$rc_file"; then
679
+ # shellcheck disable=SC2016
680
+ sed -i.bak '/case ":$PATH:" in.*\.aidevops\/bin/d' "$rc_file"
681
+ rm -f "${rc_file}.bak"
682
+ fi
683
+ # For fish: also remove old 'contains' form that only checked presence
684
+ if [[ "$is_fish_rc" == "true" ]] && grep -q 'contains.*\.aidevops/bin' "$rc_file"; then
685
+ sed -i.bak '/contains.*\.aidevops\/bin/d' "$rc_file"
686
+ rm -f "${rc_file}.bak"
687
+ fi
688
+ # Check for the new sanitize-and-prepend form (uses _aidevops_shim variable)
689
+ if ! grep -Fq '_aidevops_shim' "$rc_file"; then
690
+ {
691
+ echo ""
692
+ echo "# Added by aidevops setup (GH#2993: shellcheck shim on PATH)"
693
+ echo "$rc_path_line"
694
+ } >>"$rc_file"
695
+ fi
696
+ done < <(get_all_shell_rcs)
697
+
698
+ if [[ -n "$added_to" ]]; then
699
+ print_success "Configured SHELLCHECK_PATH wrapper in: $added_to"
700
+ fi
701
+
702
+ if [[ -n "$already_in" ]]; then
703
+ print_info "SHELLCHECK_PATH already configured in: $already_in"
704
+ fi
705
+
706
+ if [[ -z "$added_to" && -z "$already_in" && "$PLATFORM_MACOS" != "true" ]]; then
707
+ print_warning "Could not configure SHELLCHECK_PATH automatically"
708
+ print_info "Add this to your shell config: $env_line"
709
+ fi
710
+
711
+ # Also export for current session
712
+ export SHELLCHECK_PATH="$wrapper_path"
713
+ export PATH="$HOME/.aidevops/bin:$PATH"
714
+
715
+ return 0
716
+ }
717
+
718
+ # Add server access aliases to shell rc files (bash/zsh/fish)
719
+ setup_aliases() {
720
+ print_info "Setting up shell aliases..."
721
+
722
+ local default_shell
723
+ default_shell=$(detect_default_shell)
724
+
725
+ # Fish shell uses different alias syntax
726
+ local is_fish=false
727
+ if [[ "$default_shell" == "fish" ]]; then
728
+ is_fish=true
729
+ fi
730
+
731
+ local alias_block_bash
732
+ alias_block_bash=$(
733
+ cat <<'ALIASES'
734
+
735
+ # AI Assistant Server Access Framework
736
+ alias servers='./.agents/scripts/servers-helper.sh'
737
+ alias servers-list='./.agents/scripts/servers-helper.sh list'
738
+ alias hostinger='./.agents/scripts/hostinger-helper.sh'
739
+ alias hetzner='./.agents/scripts/hetzner-helper.sh'
740
+ alias aws-helper='./.agents/scripts/aws-helper.sh'
741
+ ALIASES
742
+ )
743
+
744
+ local alias_block_fish
745
+ alias_block_fish=$(
746
+ cat <<'ALIASES'
747
+
748
+ # AI Assistant Server Access Framework
749
+ alias servers './.agents/scripts/servers-helper.sh'
750
+ alias servers-list './.agents/scripts/servers-helper.sh list'
751
+ alias hostinger './.agents/scripts/hostinger-helper.sh'
752
+ alias hetzner './.agents/scripts/hetzner-helper.sh'
753
+ alias aws-helper './.agents/scripts/aws-helper.sh'
754
+ ALIASES
755
+ )
756
+
757
+ # Check if aliases already exist in any rc file (including fish config)
758
+ local any_configured=false
759
+ local rc_file
760
+ while IFS= read -r rc_file; do
761
+ [[ -z "$rc_file" ]] && continue
762
+ if [[ -f "$rc_file" ]] && grep -q "# AI Assistant Server Access" "$rc_file"; then
763
+ any_configured=true
764
+ break
765
+ fi
766
+ done < <(get_all_shell_rcs)
767
+ # Also check fish config (not included in get_all_shell_rcs on macOS)
768
+ if [[ "$any_configured" == "false" ]]; then
769
+ local fish_config="$HOME/.config/fish/config.fish"
770
+ if [[ -f "$fish_config" ]] && grep -q "# AI Assistant Server Access" "$fish_config"; then
771
+ any_configured=true
772
+ fi
773
+ fi
774
+
775
+ if [[ "$any_configured" == "true" ]]; then
776
+ print_info "Server Access aliases already configured - Skipping"
777
+ return 0
778
+ fi
779
+
780
+ print_info "Detected default shell: $default_shell"
781
+ read -r -p "Add shell aliases? [Y/n]: " add_aliases
782
+
783
+ if [[ "$add_aliases" =~ ^[Yy]?$ ]]; then
784
+ local added_to=""
785
+
786
+ # Handle fish separately
787
+ if [[ "$is_fish" == "true" ]]; then
788
+ local fish_rc="$HOME/.config/fish/config.fish"
789
+ mkdir -p "$HOME/.config/fish"
790
+ echo "$alias_block_fish" >>"$fish_rc"
791
+ added_to="$fish_rc"
792
+ else
793
+ # Add to all bash/zsh rc files
794
+ while IFS= read -r rc_file; do
795
+ [[ -z "$rc_file" ]] && continue
796
+
797
+ # Create if it doesn't exist
798
+ if [[ ! -f "$rc_file" ]]; then
799
+ touch "$rc_file"
800
+ fi
801
+
802
+ # Skip if already has aliases (file created above if it didn't exist)
803
+ if grep -q "# AI Assistant Server Access" "$rc_file"; then
804
+ continue
805
+ fi
806
+
807
+ echo "$alias_block_bash" >>"$rc_file"
808
+ added_to="${added_to:+$added_to, }$rc_file"
809
+ done < <(get_all_shell_rcs)
810
+ fi
811
+
812
+ if [[ -n "$added_to" ]]; then
813
+ print_success "Aliases added to: $added_to"
814
+ print_info "Restart your terminal to use aliases"
815
+ fi
816
+ else
817
+ print_info "Skipped alias setup by user request"
818
+ fi
819
+ return 0
820
+ }
821
+
822
+ # Install terminal title integration that syncs tab titles with git repo/branch
823
+ setup_terminal_title() {
824
+ print_info "Setting up terminal title integration..."
825
+
826
+ local setup_script=".agents/scripts/terminal-title-setup.sh"
827
+
828
+ if [[ ! -f "$setup_script" ]]; then
829
+ print_warning "Terminal title setup script not found - skipping"
830
+ return 0
831
+ fi
832
+
833
+ # Check if already installed (check all rc files)
834
+ local title_configured=false
835
+ local rc_file
836
+ while IFS= read -r rc_file; do
837
+ [[ -z "$rc_file" ]] && continue
838
+ if [[ -f "$rc_file" ]] && grep -q "aidevops terminal-title" "$rc_file"; then
839
+ title_configured=true
840
+ break
841
+ fi
842
+ done < <(get_all_shell_rcs)
843
+
844
+ if [[ "$title_configured" == "true" ]]; then
845
+ print_info "Terminal title integration already configured - Skipping"
846
+ return 0
847
+ fi
848
+
849
+ # Show current status before asking
850
+ echo ""
851
+ print_info "Terminal title integration syncs your terminal tab with git repo/branch"
852
+ print_info "Example: Tab shows 'aidevops/feature/xyz' when in that branch"
853
+ echo ""
854
+ echo "Current status:"
855
+
856
+ # Shell info
857
+ local shell_name
858
+ shell_name=$(detect_default_shell)
859
+ local shell_info="$shell_name"
860
+ if [[ "$shell_name" == "zsh" ]] && [[ -d "$HOME/.oh-my-zsh" ]]; then
861
+ shell_info="$shell_name (Oh-My-Zsh)"
862
+ fi
863
+ echo " Shell: $shell_info"
864
+
865
+ # Tabby info
866
+ local tabby_config="$HOME/Library/Application Support/tabby/config.yaml"
867
+ if [[ -f "$tabby_config" ]]; then
868
+ local disabled_count
869
+ disabled_count=$(grep -c "disableDynamicTitle: true" "$tabby_config" || echo "0")
870
+ if [[ "$disabled_count" -gt 0 ]]; then
871
+ echo " Tabby: detected, dynamic titles disabled in $disabled_count profile(s) (will fix)"
872
+ else
873
+ echo " Tabby: detected, dynamic titles enabled"
874
+ fi
875
+ fi
876
+
877
+ echo ""
878
+ read -r -p "Install terminal title integration? [Y/n]: " install_title
879
+
880
+ if [[ "$install_title" =~ ^[Yy]?$ ]]; then
881
+ if bash "$setup_script" install; then
882
+ print_success "Terminal title integration installed"
883
+ else
884
+ print_warning "Terminal title setup encountered issues (non-critical)"
885
+ fi
886
+ else
887
+ print_info "Skipped terminal title setup by user request"
888
+ print_info "You can install later with: ~/.aidevops/agents/scripts/terminal-title-setup.sh install"
889
+ fi
890
+
891
+ return 0
892
+ }