aidevops 3.13.95 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1240 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
3
- # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
- # Shell environment setup functions: oh-my-zsh, shell-compat, aliases, terminal-title
5
- # Part of aidevops setup.sh modularization (t316.3)
6
-
7
- # Shell safety baseline
8
- set -Eeuo pipefail
9
- IFS=$'\n\t'
10
- # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
11
- trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
12
- shopt -s inherit_errexit 2>/dev/null || true
13
-
14
- # Detect the shell currently executing this script (zsh, bash, or fallback)
15
- detect_running_shell() {
16
- if [[ -n "${ZSH_VERSION:-}" ]]; then
17
- echo "zsh"
18
- elif [[ -n "${BASH_VERSION:-}" ]]; then
19
- echo "bash"
20
- else
21
- basename "${SHELL:-/bin/bash}"
22
- fi
23
- return 0
24
- }
25
-
26
- # Detect the user's default login shell from the system (not $SHELL env var,
27
- # which reflects the current shell, not the system default).
28
- detect_default_shell() {
29
- local user_name shell_path
30
- user_name="$(id -un 2>/dev/null || whoami)"
31
- shell_path=""
32
-
33
- if [[ "$(uname)" == "Darwin" ]]; then
34
- shell_path="$(dscl . -read "/Users/$user_name" UserShell 2>/dev/null | awk 'NR==1 {print $2}' || true)"
35
- else
36
- if command -v getent >/dev/null 2>&1; then
37
- shell_path="$(getent passwd "$user_name" 2>/dev/null | cut -d: -f7 || true)"
38
- fi
39
- fi
40
-
41
- # Fallback to $SHELL if system query returned empty
42
- [[ -z "$shell_path" ]] && shell_path="${SHELL:-/bin/bash}"
43
- basename "$shell_path"
44
- return 0
45
- }
46
-
47
- # Usage: get_shell_rc "zsh" or get_shell_rc "bash"
48
- get_shell_rc() {
49
- local shell_name
50
- shell_name="$1"
51
- case "$shell_name" in
52
- zsh)
53
- echo "$HOME/.zshrc"
54
- ;;
55
- bash)
56
- if [[ "$(uname)" == "Darwin" ]]; then
57
- echo "$HOME/.bash_profile"
58
- else
59
- echo "$HOME/.bashrc"
60
- fi
61
- ;;
62
- fish)
63
- echo "$HOME/.config/fish/config.fish"
64
- ;;
65
- ksh)
66
- echo "$HOME/.kshrc"
67
- ;;
68
- *)
69
- # Fallback: check common rc files
70
- if [[ -f "$HOME/.zshrc" ]]; then
71
- echo "$HOME/.zshrc"
72
- elif [[ -f "$HOME/.bashrc" ]]; then
73
- echo "$HOME/.bashrc"
74
- elif [[ -f "$HOME/.bash_profile" ]]; then
75
- echo "$HOME/.bash_profile"
76
- else
77
- echo ""
78
- fi
79
- ;;
80
- esac
81
- return 0
82
- }
83
-
84
- # Return all relevant shell rc file paths for the current platform
85
- get_all_shell_rcs() {
86
- local rcs=()
87
-
88
- if [[ "$(uname)" == "Darwin" ]]; then
89
- # macOS: always include zsh (default since Catalina) and bash_profile
90
- [[ -f "$HOME/.zshrc" ]] && rcs+=("$HOME/.zshrc")
91
- [[ -f "$HOME/.bash_profile" ]] && rcs+=("$HOME/.bash_profile")
92
- # If neither exists, create .zshrc (macOS default)
93
- if [[ ${#rcs[@]} -eq 0 ]]; then
94
- touch "$HOME/.zshrc"
95
- rcs+=("$HOME/.zshrc")
96
- fi
97
- else
98
- # Linux: use the default shell's rc file
99
- local default_shell
100
- default_shell=$(detect_default_shell)
101
- local rc
102
- rc=$(get_shell_rc "$default_shell")
103
- if [[ -n "$rc" ]]; then
104
- rcs+=("$rc")
105
- fi
106
- fi
107
-
108
- printf '%s\n' "${rcs[@]}"
109
- return 0
110
- }
111
-
112
- # Offer to change the default shell to zsh after Oh My Zsh is installed.
113
- _omz_offer_shell_change() {
114
- local default_shell="$1"
115
-
116
- if [[ "$default_shell" == "zsh" ]]; then
117
- return 0
118
- fi
119
-
120
- echo ""
121
- setup_prompt change_shell "Change default shell to zsh? [y/N]: " "n"
122
- if [[ "$change_shell" =~ ^[Yy]$ ]]; then
123
- if chsh -s "$(command -v zsh)"; then
124
- print_success "Default shell changed to zsh"
125
- print_info "Restart your terminal for the change to take effect"
126
- else
127
- print_warning "Failed to change shell - run manually: chsh -s $(command -v zsh)"
128
- fi
129
- fi
130
-
131
- return 0
132
- }
133
-
134
- # Run the Oh My Zsh installer and handle post-install steps.
135
- _omz_run_install() {
136
- local default_shell="$1"
137
-
138
- print_info "Installing Oh My Zsh..."
139
- # Use verified download + --unattended to avoid changing the shell or starting zsh
140
- # shellcheck disable=SC2034 # Read by verified_install() in setup.sh
141
- VERIFIED_INSTALL_SHELL="sh"
142
- if verified_install "Oh My Zsh" "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" --unattended; then
143
- print_success "Oh My Zsh installed"
144
- if [[ ! -f "$HOME/.zshrc" ]]; then
145
- print_warning ".zshrc not created - Oh My Zsh may not have installed correctly"
146
- fi
147
- _omz_offer_shell_change "$default_shell"
148
- else
149
- print_warning "Oh My Zsh installation failed"
150
- 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"
151
- fi
152
-
153
- return 0
154
- }
155
-
156
- # Offer to install Oh My Zsh if zsh is the default shell and OMZ is not present
157
- setup_oh_my_zsh() {
158
- # Check prerequisites before announcing setup (GH#5240)
159
- if ! command -v zsh >/dev/null 2>&1; then
160
- print_skip "Oh My Zsh" "zsh not installed" "Install zsh first, then re-run setup"
161
- setup_track_skipped "Oh My Zsh" "zsh not installed"
162
- return 0
163
- fi
164
-
165
- if [[ -d "$HOME/.oh-my-zsh" ]]; then
166
- print_success "Oh My Zsh already installed"
167
- setup_track_configured "Oh My Zsh"
168
- return 0
169
- fi
170
-
171
- local default_shell
172
- default_shell=$(detect_default_shell)
173
-
174
- if [[ "$default_shell" != "zsh" && "$(uname)" != "Darwin" ]]; then
175
- print_skip "Oh My Zsh" "default shell is $default_shell (not zsh)" "Change default shell to zsh: chsh -s \$(which zsh)"
176
- setup_track_skipped "Oh My Zsh" "default shell is $default_shell"
177
- return 0
178
- fi
179
-
180
- print_info "Oh My Zsh enhances zsh with themes, plugins, and completions"
181
- echo " Many tools installed later (git, fd, brew) benefit from Oh My Zsh plugins."
182
- echo " This is optional - plain zsh works fine without it."
183
- echo ""
184
-
185
- setup_prompt install_omz "Install Oh My Zsh? [y/N]: " "n"
186
-
187
- if [[ "$install_omz" =~ ^[Yy]$ ]]; then
188
- _omz_run_install "$default_shell"
189
- else
190
- print_info "Skipped Oh My Zsh installation"
191
- 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"
192
- fi
193
-
194
- return 0
195
- }
196
-
197
- # Check that both zsh and bash are installed and collect existing bash config files.
198
- # Prints the bash config file paths (one per line) on success.
199
- # Returns 1 if prerequisites are not met (caller should return 0 early).
200
- _shell_compat_check_prereqs() {
201
- local shared_profile="$1"
202
- local zsh_rc="$2"
203
-
204
- if [[ -f "$shared_profile" ]]; then
205
- print_success "Cross-shell compatibility already configured ($shared_profile)"
206
- return 1
207
- fi
208
-
209
- if ! command -v zsh >/dev/null 2>&1; then
210
- print_info "zsh not installed - cross-shell setup not needed"
211
- return 1
212
- fi
213
- if ! command -v bash >/dev/null 2>&1; then
214
- print_info "bash not installed - cross-shell setup not needed"
215
- return 1
216
- fi
217
-
218
- # Collect all bash config files that exist
219
- # macOS: .bash_profile (login) + .bashrc (interactive, often sourced by .bash_profile)
220
- # Linux: .bashrc (primary) + .bash_profile (login, often sources .bashrc)
221
- # We check all of them on both platforms since tools write to either
222
- local -a bash_files=()
223
- [[ -f "$HOME/.bash_profile" ]] && bash_files+=("$HOME/.bash_profile")
224
- [[ -f "$HOME/.bashrc" ]] && bash_files+=("$HOME/.bashrc")
225
- [[ -f "$HOME/.profile" ]] && bash_files+=("$HOME/.profile")
226
-
227
- if [[ ${#bash_files[@]} -eq 0 ]]; then
228
- print_info "No bash config files found - skipping cross-shell setup"
229
- return 1
230
- fi
231
-
232
- if [[ ! -f "$zsh_rc" ]]; then
233
- print_info "No .zshrc found - skipping cross-shell setup"
234
- return 1
235
- fi
236
-
237
- printf '%s\n' "${bash_files[@]}"
238
- return 0
239
- }
240
-
241
- # Count portable customizations (exports, aliases, PATH entries) across bash config files.
242
- # Prints "exports aliases paths" space-separated.
243
- _shell_compat_count_customizations() {
244
- local total_exports=0
245
- local total_aliases=0
246
- local total_paths=0
247
-
248
- local src_file
249
- for src_file in "$@"; do
250
- local n
251
- # grep -c exits 1 on no match; || : prevents ERR trap noise
252
- # File existence already verified when building bash_files array
253
- n=$(grep -cE '^\s*export\s+[A-Z]' "$src_file" || :)
254
- total_exports=$((total_exports + ${n:-0}))
255
- n=$(grep -cE '^\s*alias\s+' "$src_file" || :)
256
- total_aliases=$((total_aliases + ${n:-0}))
257
- n=$(grep -cE 'PATH.*=' "$src_file" || :)
258
- total_paths=$((total_paths + ${n:-0}))
259
- done
260
-
261
- echo "$total_exports $total_aliases $total_paths"
262
- return 0
263
- }
264
-
265
- # Return 0 if the line is bash-specific and should NOT be extracted to shared profile.
266
- _shell_compat_is_bash_specific() {
267
- local line="$1"
268
- case "$line" in
269
- *shopt*) return 0 ;;
270
- *PROMPT_COMMAND*) return 0 ;;
271
- *PS1=*) return 0 ;;
272
- *PS2=*) return 0 ;;
273
- *bash_completion*) return 0 ;;
274
- *"complete "*) return 0 ;;
275
- *"bind "*) return 0 ;;
276
- *HISTCONTROL*) return 0 ;;
277
- *HISTFILESIZE*) return 0 ;;
278
- *HISTSIZE*) return 0 ;;
279
- *"source /etc/bash"*) return 0 ;;
280
- *". /etc/bash"*) return 0 ;;
281
- *"source /etc/profile"*) return 0 ;;
282
- *". /etc/profile"*) return 0 ;;
283
- # Skip lines that source .bashrc from .bash_profile (circular)
284
- *".bashrc"*) return 0 ;;
285
- # Skip lines that source .shell_common (we'll add this ourselves)
286
- *"shell_common"*) return 0 ;;
287
- esac
288
- return 1
289
- }
290
-
291
- # Return 0 if the line is a portable shell customization (exports, aliases, PATH, etc.).
292
- _shell_compat_is_portable() {
293
- local line="$1"
294
- case "$line" in
295
- export\ [A-Z]* | export\ PATH*) return 0 ;;
296
- alias\ *) return 0 ;;
297
- eval\ *) return 0 ;;
298
- *PATH=*) return 0 ;;
299
- # Also match 'source' and '. ' commands (tool integrations like nvm, rvm, pyenv)
300
- source\ * | .\ /* | .\ \$* | .\ \~*) return 0 ;;
301
- esac
302
- return 1
303
- }
304
-
305
- # Write the header block for the shared profile file.
306
- _shell_compat_write_profile_header() {
307
- local shared_profile="$1"
308
-
309
- {
310
- echo "# Shared shell profile - sourced by both bash and zsh"
311
- echo "# Created by aidevops setup to preserve customizations across shell switches"
312
- echo "# Edit this file for settings you want in BOTH bash and zsh"
313
- echo "# Shell-specific settings go in ~/.bashrc or ~/.zshrc"
314
- echo ""
315
- } >"$shared_profile"
316
-
317
- return 0
318
- }
319
-
320
- # Extract portable lines from one source file into the shared profile.
321
- # Skips duplicates (tracked via seen_lines nameref-style via global).
322
- # Prints the number of lines extracted from this file.
323
- # Args: shared_profile src_file [seen_line1 seen_line2 ...]
324
- # Returns extracted count via stdout; seen lines via stdout after count (newline-separated).
325
- _shell_compat_extract_one_file() {
326
- local shared_profile="$1"
327
- local src_file="$2"
328
- shift 2
329
- # remaining args are already-seen lines
330
-
331
- local src_basename
332
- src_basename=$(basename "$src_file")
333
- local added_header=false
334
- local extracted=0
335
-
336
- # Build a local seen set from passed args
337
- local -a seen_lines=("$@")
338
-
339
- while IFS= read -r line || [[ -n "$line" ]]; do
340
- [[ -z "$line" ]] && continue
341
- [[ "$line" =~ ^[[:space:]]*# ]] && continue
342
- _shell_compat_is_bash_specific "$line" && continue
343
- _shell_compat_is_portable "$line" || continue
344
-
345
- # Deduplicate
346
- local is_dup=false
347
- local seen
348
- for seen in "${seen_lines[@]+"${seen_lines[@]}"}"; do
349
- if [[ "$seen" == "$line" ]]; then
350
- is_dup=true
351
- break
352
- fi
353
- done
354
- [[ "$is_dup" == "true" ]] && continue
355
-
356
- if [[ "$added_header" == "false" ]]; then
357
- echo "" >>"$shared_profile"
358
- echo "# From $src_basename" >>"$shared_profile"
359
- added_header=true
360
- fi
361
- echo "$line" >>"$shared_profile"
362
- seen_lines+=("$line")
363
- ((++extracted))
364
- done <"$src_file"
365
-
366
- # Output: count on first line, then new seen lines (for caller to accumulate)
367
- echo "$extracted"
368
- printf '%s\n' "${seen_lines[@]+"${seen_lines[@]}"}"
369
- return 0
370
- }
371
-
372
- # Extract portable customizations from bash config files into the shared profile.
373
- # Returns the count of extracted lines.
374
- _shell_compat_extract_to_shared_profile() {
375
- local shared_profile="$1"
376
- shift
377
- # remaining args are bash_files
378
-
379
- _shell_compat_write_profile_header "$shared_profile"
380
-
381
- local -a seen_lines=()
382
- local extracted=0
383
-
384
- local src_file
385
- for src_file in "$@"; do
386
- local file_out
387
- file_out=$(_shell_compat_extract_one_file "$shared_profile" "$src_file" "${seen_lines[@]+"${seen_lines[@]}"}")
388
- local file_count
389
- file_count=$(printf '%s\n' "$file_out" | head -1)
390
- extracted=$((extracted + file_count))
391
- # Rebuild seen_lines from output (skip first line = count)
392
- local -a new_seen=()
393
- while IFS= read -r seen_line; do
394
- [[ -n "$seen_line" ]] && new_seen+=("$seen_line")
395
- done < <(printf '%s\n' "$file_out" | tail -n +2)
396
- seen_lines=("${new_seen[@]+"${new_seen[@]}"}")
397
- done
398
-
399
- echo "$extracted"
400
- return 0
401
- }
402
-
403
- # Add sourcing of the shared profile to .zshrc and all bash config files.
404
- _shell_compat_add_sourcing() {
405
- local zsh_rc="$1"
406
- shift
407
- # remaining args are bash_files
408
-
409
- # Add sourcing to .zshrc if not already present (existence verified above)
410
- if ! grep -q 'shell_common' "$zsh_rc"; then
411
- {
412
- echo ""
413
- echo "# Cross-shell compatibility (added by aidevops setup)"
414
- echo "# Sources shared profile so bash customizations work in zsh too"
415
- # shellcheck disable=SC2016
416
- echo '[ -f "$HOME/.shell_common" ] && . "$HOME/.shell_common"'
417
- } >>"$zsh_rc"
418
- print_success "Added shared profile sourcing to .zshrc"
419
- fi
420
-
421
- # Add sourcing to bash config files if not already present
422
- # File existence already verified when building bash_files array
423
- local src_file
424
- for src_file in "$@"; do
425
- if ! grep -q 'shell_common' "$src_file"; then
426
- {
427
- echo ""
428
- echo "# Cross-shell compatibility (added by aidevops setup)"
429
- echo "# Shared profile - edit ~/.shell_common for settings in both shells"
430
- # shellcheck disable=SC2016
431
- echo '[ -f "$HOME/.shell_common" ] && . "$HOME/.shell_common"'
432
- } >>"$src_file"
433
- print_success "Added shared profile sourcing to $(basename "$src_file")"
434
- fi
435
- done
436
-
437
- return 0
438
- }
439
-
440
- # Print detected customization counts and prompt the user.
441
- # Returns 1 if the user declines (caller should return 0).
442
- _shell_compat_prompt_user() {
443
- local file_count="$1"
444
- local total_exports="$2"
445
- local total_aliases="$3"
446
- local total_paths="$4"
447
-
448
- print_info "Detected bash customizations across ${file_count} file(s):"
449
- echo " Exports: $total_exports, Aliases: $total_aliases, PATH entries: $total_paths"
450
- echo ""
451
- print_info "Best practice: create a shared profile (~/.shell_common) sourced by"
452
- print_info "both bash and zsh, so your customizations work in either shell."
453
- echo ""
454
-
455
- local setup_compat
456
- setup_prompt setup_compat "Create shared shell profile for cross-shell compatibility? [Y/n]: " "Y"
457
-
458
- if [[ ! "$setup_compat" =~ ^[Yy]?$ ]]; then
459
- print_info "Skipped cross-shell compatibility setup"
460
- print_info "Set up later by creating ~/.shell_common and sourcing it from both shells"
461
- return 1
462
- fi
463
-
464
- return 0
465
- }
466
-
467
- # Do the extraction and sourcing steps, then print the success summary.
468
- _shell_compat_do_extract() {
469
- local shared_profile="$1"
470
- local zsh_rc="$2"
471
- shift 2
472
- # remaining args are bash_files
473
-
474
- # We extract: exports, PATH modifications, aliases, eval statements, source commands
475
- # We skip: bash-specific syntax (shopt, PROMPT_COMMAND, PS1, completion, bind, etc.)
476
- # We deduplicate lines that appear in multiple files (e.g. .bash_profile sources .bashrc)
477
- print_info "Creating shared profile: $shared_profile"
478
-
479
- local extracted
480
- extracted=$(_shell_compat_extract_to_shared_profile "$shared_profile" "$@")
481
-
482
- if [[ $extracted -eq 0 ]]; then
483
- rm -f "$shared_profile"
484
- print_info "No portable customizations found to extract"
485
- return 0
486
- fi
487
-
488
- chmod 644 "$shared_profile"
489
- print_success "Extracted $extracted unique customization(s) to $shared_profile"
490
- _shell_compat_add_sourcing "$zsh_rc" "$@"
491
-
492
- echo ""
493
- print_success "Cross-shell compatibility configured"
494
- print_info "Your customizations are now in: $shared_profile"
495
- print_info "Both bash and zsh will source this file automatically."
496
- print_info "Edit ~/.shell_common for settings you want in both shells."
497
- print_info "Use ~/.bashrc or ~/.zshrc for shell-specific settings only."
498
-
499
- return 0
500
- }
501
-
502
- # Extract portable customizations from bash configs into a shared profile for cross-shell use
503
- setup_shell_compatibility() {
504
- print_info "Setting up cross-shell compatibility..."
505
-
506
- local shared_profile="$HOME/.shell_common"
507
- local zsh_rc="$HOME/.zshrc"
508
-
509
- # Check prerequisites; collect bash config files
510
- local -a bash_files=()
511
- local prereq_out
512
- prereq_out=$(_shell_compat_check_prereqs "$shared_profile" "$zsh_rc") || return 0
513
- while IFS= read -r line; do
514
- [[ -n "$line" ]] && bash_files+=("$line")
515
- done <<<"$prereq_out"
516
-
517
- # Count customizations across all bash config files
518
- local counts
519
- counts=$(_shell_compat_count_customizations "${bash_files[@]}")
520
- local total_exports total_aliases total_paths
521
- read -r total_exports total_aliases total_paths <<<"$counts"
522
-
523
- if [[ $total_exports -eq 0 && $total_aliases -eq 0 && $total_paths -eq 0 ]]; then
524
- print_info "No bash customizations detected - skipping cross-shell setup"
525
- return 0
526
- fi
527
-
528
- _shell_compat_prompt_user "${#bash_files[@]}" "$total_exports" "$total_aliases" "$total_paths" || return 0
529
- _shell_compat_do_extract "$shared_profile" "$zsh_rc" "${bash_files[@]}"
530
-
531
- return 0
532
- }
533
-
534
- # Check for optional dependencies (sshpass) and offer to install them
535
- check_optional_deps() {
536
- print_info "Checking optional dependencies..."
537
-
538
- local missing_optional=()
539
-
540
- if ! command -v sshpass >/dev/null 2>&1; then
541
- missing_optional+=("sshpass")
542
- else
543
- print_success "sshpass found"
544
- fi
545
-
546
- check_python_version "" "skills/tools" >/dev/null || true
547
-
548
- if [[ ${#missing_optional[@]} -gt 0 ]]; then
549
- print_warning "Missing optional dependencies: ${missing_optional[*]}"
550
- echo " sshpass - needed for password-based SSH (like Hostinger)"
551
-
552
- local pkg_manager
553
- pkg_manager=$(detect_package_manager)
554
-
555
- if [[ "$pkg_manager" != "unknown" ]]; then
556
- setup_prompt install_optional "Install optional dependencies using $pkg_manager? [Y/n]: " "Y"
557
-
558
- if [[ "$install_optional" =~ ^[Yy]?$ ]]; then
559
- print_info "Installing ${missing_optional[*]}..."
560
- if install_packages "$pkg_manager" "${missing_optional[@]}"; then
561
- print_success "Optional dependencies installed"
562
- else
563
- print_warning "Failed to install optional dependencies (non-critical)"
564
- fi
565
- else
566
- print_info "Skipped optional dependencies"
567
- fi
568
- fi
569
- fi
570
- return 0
571
- }
572
-
573
- # Update a single rc file to include ~/.local/bin on PATH.
574
- # Prints "added:<file>" or "already:<file>" to stdout.
575
- _local_bin_update_rc_file() {
576
- local rc_file="$1"
577
- local path_line="$2"
578
-
579
- # Create the rc file if it doesn't exist (ensure parent dir exists for fish etc.)
580
- if [[ ! -f "$rc_file" ]]; then
581
- mkdir -p "$(dirname "$rc_file")"
582
- touch "$rc_file"
583
- fi
584
-
585
- # Check if already added (file created above if it didn't exist)
586
- if grep -q '\.local/bin' "$rc_file"; then
587
- echo "already:$rc_file"
588
- return 0
589
- fi
590
-
591
- # Add to shell config
592
- {
593
- echo ""
594
- echo "# Added by aidevops setup"
595
- echo "$path_line"
596
- } >>"$rc_file"
597
- echo "added:$rc_file"
598
- return 0
599
- }
600
-
601
- # Print result messages for add_local_bin_to_path.
602
- _local_bin_report_results() {
603
- local added_to="$1"
604
- local already_in="$2"
605
- local path_line="$3"
606
-
607
- if [[ -n "$added_to" ]]; then
608
- print_success "Added $HOME/.local/bin to PATH in: $added_to"
609
- print_info "Restart your terminal to use 'aidevops' command"
610
- fi
611
-
612
- if [[ -n "$already_in" ]]; then
613
- print_info "$HOME/.local/bin already in PATH in: $already_in"
614
- fi
615
-
616
- if [[ -z "$added_to" && -z "$already_in" ]]; then
617
- print_warning "Could not detect shell config file"
618
- print_info "Add this to your shell config: $path_line"
619
- fi
620
-
621
- return 0
622
- }
623
-
624
- # Add ~/.local/bin to PATH in all shell rc files for the aidevops CLI
625
- add_local_bin_to_path() {
626
- # shellcheck disable=SC2016 # path_line is written to rc files; must expand at shell startup, not now
627
- local path_line='export PATH="$HOME/.local/bin:$PATH"'
628
- local added_to=""
629
- local already_in=""
630
-
631
- local rc_file
632
- while IFS= read -r rc_file; do
633
- [[ -z "$rc_file" ]] && continue
634
- local result
635
- result=$(_local_bin_update_rc_file "$rc_file" "$path_line")
636
- case "$result" in
637
- added:*) added_to="${added_to:+$added_to, }${result#added:}" ;;
638
- already:*) already_in="${already_in:+$already_in, }${result#already:}" ;;
639
- esac
640
- done < <(get_all_shell_rcs)
641
-
642
- _local_bin_report_results "$added_to" "$already_in" "$path_line"
643
-
644
- # Also export for current session
645
- export PATH="$HOME/.local/bin:$PATH"
646
-
647
- return 0
648
- }
649
-
650
- # GH#2915, GH#2993: Ensure all processes use the safe ShellCheck wrapper.
651
- #
652
- # The bash language server hardcodes --external-sources in every ShellCheck
653
- # invocation, causing exponential memory growth (9+ GB) when source chains
654
- # span 463+ scripts. The wrapper strips --external-sources and enforces a
655
- # background RSS watchdog (ulimit -v is broken on macOS ARM — EINVAL).
656
- #
657
- # GH#2915 set SHELLCHECK_PATH env var, but bash-language-server ignores it —
658
- # it resolves `shellcheck` via PATH lookup, finding /opt/homebrew/bin/shellcheck
659
- # directly. GH#2993 fixes this by placing a shim on PATH ahead of the real binary.
660
- #
661
- # Four layers ensure all processes use the wrapper:
662
- # 0. PATH shim — ~/.aidevops/bin/shellcheck symlink + PATH prepend (GH#2993)
663
- # 1. launchctl setenv (macOS) — GUI-launched apps (current boot only)
664
- # 2. .zshenv — ALL zsh processes including non-interactive (persists)
665
- # 3. Shell rc files (.zshrc, .bash_profile) — interactive terminals
666
- #
667
- # Layer 0 is the primary fix: bash-language-server does a PATH lookup for
668
- # "shellcheck". By placing ~/.aidevops/bin first on PATH with a symlink to
669
- # the wrapper, the language server finds the wrapper instead of the real binary.
670
- # Layers 1-3 are retained for tools that honour SHELLCHECK_PATH.
671
- #
672
- # CRITICAL: ~/.aidevops/bin MUST be at the START of PATH, not the end.
673
- # If it appears after /opt/homebrew/bin, the real shellcheck is found first
674
- # and the wrapper is bypassed entirely. The launchctl setenv always prepends,
675
- # and the case-guard in shell rc files ensures it stays first.
676
-
677
- # Verify the shellcheck wrapper exists and is executable.
678
- # Returns 1 if setup should be aborted (caller returns 0).
679
- _shellcheck_wrapper_verify() {
680
- local wrapper_path="$1"
681
-
682
- if [[ ! -x "$wrapper_path" ]]; then
683
- if [[ -f "$wrapper_path" ]]; then
684
- chmod +x "$wrapper_path"
685
- else
686
- print_warning "ShellCheck wrapper not found at $wrapper_path (will be available after deploy)"
687
- return 1
688
- fi
689
- fi
690
-
691
- if ! "$wrapper_path" --version >/dev/null 2>&1; then
692
- print_warning "ShellCheck wrapper cannot find real shellcheck binary — skipping"
693
- return 1
694
- fi
695
-
696
- return 0
697
- }
698
-
699
- # Layer 0: Create the PATH shim (GH#2993).
700
- # Places a symlink to the wrapper in ~/.aidevops/bin so bash-language-server
701
- # finds the wrapper via PATH lookup before the real shellcheck binary.
702
- _shellcheck_wrapper_setup_shim() {
703
- local wrapper_path="$1"
704
- local shim_dir="$HOME/.aidevops/bin"
705
- local shim_path="$shim_dir/shellcheck"
706
-
707
- mkdir -p "$shim_dir"
708
-
709
- local wrapper_realpath
710
- wrapper_realpath="$(realpath "$wrapper_path" 2>/dev/null || readlink -f "$wrapper_path" 2>/dev/null || echo "$wrapper_path")"
711
-
712
- if [[ -L "$shim_path" ]]; then
713
- local current_target
714
- current_target="$(realpath "$shim_path" 2>/dev/null || readlink -f "$shim_path" 2>/dev/null || echo "")"
715
- if [[ "$current_target" != "$wrapper_realpath" ]]; then
716
- ln -sf "$wrapper_path" "$shim_path"
717
- print_info "Updated shellcheck shim symlink: $shim_path → $wrapper_path"
718
- fi
719
- elif [[ -e "$shim_path" ]]; then
720
- # Regular file exists — back up and replace with symlink
721
- mv "$shim_path" "${shim_path}.bak.$(date +%Y%m%d_%H%M%S)"
722
- ln -sf "$wrapper_path" "$shim_path"
723
- print_info "Replaced shellcheck shim with symlink: $shim_path → $wrapper_path"
724
- else
725
- ln -sf "$wrapper_path" "$shim_path"
726
- print_success "Created shellcheck shim: $shim_path → $wrapper_path"
727
- fi
728
-
729
- return 0
730
- }
731
-
732
- # Layer 1: Set SHELLCHECK_PATH and prepend shim dir via launchctl (macOS only).
733
- # Affects all GUI-launched processes for the current boot session.
734
- _shellcheck_wrapper_setup_launchctl() {
735
- local wrapper_path="$1"
736
- local shim_dir="$2"
737
-
738
- # Note: 2>/dev/null on launchctl is intentional — launchctl may not be
739
- # available in non-GUI contexts (SSH, containers). Unlike grep where we
740
- # want errors visible, launchctl failure is a non-fatal fallback.
741
- if launchctl setenv SHELLCHECK_PATH "$wrapper_path" 2>/dev/null; then
742
- print_info "Set SHELLCHECK_PATH via launchctl (GUI processes)"
743
- fi
744
-
745
- # Build a clean PATH with shim_dir at the front, removing any existing
746
- # occurrence to prevent duplicates while ensuring first position.
747
- # Handle the empty-PATH edge case to avoid a trailing colon (which
748
- # resolves to "." and is a PATH injection vector).
749
- local clean_path
750
- clean_path=$(printf '%s' "$PATH" | tr ':' '\n' | grep -Fxv "$shim_dir" | tr '\n' ':' | sed 's/:$//')
751
- local new_path
752
- if [[ -n "$clean_path" ]]; then
753
- new_path="${shim_dir}:${clean_path}"
754
- else
755
- new_path="${shim_dir}"
756
- fi
757
- if launchctl setenv PATH "$new_path" 2>/dev/null; then
758
- print_info "Prepended $shim_dir to PATH via launchctl (GUI processes)"
759
- fi
760
-
761
- return 0
762
- }
763
-
764
- # Layer 2: Configure SHELLCHECK_PATH and PATH shim in .zshenv.
765
- # Affects ALL zsh processes including non-interactive (e.g. bash-language-server).
766
- _shellcheck_wrapper_setup_zshenv() {
767
- local env_line="$1"
768
- local path_line="$2"
769
- local shim_dir="$3"
770
- local zshenv="$HOME/.zshenv"
771
- local added_to_out=""
772
- local already_in_out=""
773
-
774
- if [[ -f "$zshenv" ]] || command -v zsh >/dev/null 2>&1; then
775
- touch "$zshenv"
776
-
777
- # SHELLCHECK_PATH env var (for tools that honour it)
778
- if grep -q 'SHELLCHECK_PATH' "$zshenv"; then
779
- already_in_out="$zshenv"
780
- else
781
- {
782
- echo ""
783
- echo "# Added by aidevops setup (GH#2915: prevent ShellCheck memory explosion)"
784
- echo "$env_line"
785
- } >>"$zshenv"
786
- added_to_out="$zshenv"
787
- fi
788
-
789
- # PATH prepend for ~/.aidevops/bin (GH#2993: shim must be on PATH)
790
- # Remove stale old-form entries (case guard that only checked presence,
791
- # not position — left the shim at the end of PATH on upgrades)
792
- # shellcheck disable=SC2016 # Matching literal $PATH text in rc files, not expanding
793
- if grep -q 'case ":$PATH:" in.*\.aidevops/bin' "$zshenv"; then
794
- # Remove the old case-guard line (sed is appropriate here — targeted single-line removal)
795
- # shellcheck disable=SC2016
796
- sed -i.bak '/case ":$PATH:" in.*\.aidevops\/bin/d' "$zshenv"
797
- rm -f "${zshenv}.bak"
798
- fi
799
- # Use exact-line match for the new sanitize-and-prepend form
800
- if ! grep -Fq '_aidevops_shim' "$zshenv"; then
801
- {
802
- echo ""
803
- echo "# Added by aidevops setup (GH#2993: shellcheck shim on PATH)"
804
- echo "$path_line"
805
- } >>"$zshenv"
806
- print_success "Prepended $shim_dir to PATH in .zshenv"
807
- else
808
- print_info "$shim_dir already on PATH in .zshenv"
809
- fi
810
- fi
811
-
812
- # Return values via stdout (space-separated)
813
- echo "$added_to_out $already_in_out"
814
- return 0
815
- }
816
-
817
- # Remove stale old-form PATH entries for the aidevops shim from an rc file.
818
- # Handles both bash/zsh case-guard form and fish 'contains' form.
819
- _shellcheck_wrapper_remove_stale_path() {
820
- local rc_file="$1"
821
- local is_fish_rc="$2"
822
-
823
- # Remove stale old-form entries (case guard that only checked presence,
824
- # not position — left the shim at the end of PATH on upgrades)
825
- # shellcheck disable=SC2016 # Matching literal $PATH text in rc files, not expanding
826
- if grep -q 'case ":$PATH:" in.*\.aidevops/bin' "$rc_file"; then
827
- # shellcheck disable=SC2016
828
- sed -i.bak '/case ":$PATH:" in.*\.aidevops\/bin/d' "$rc_file"
829
- rm -f "${rc_file}.bak"
830
- fi
831
- # For fish: also remove old 'contains' form that only checked presence
832
- if [[ "$is_fish_rc" == "true" ]] && grep -q 'contains.*\.aidevops/bin' "$rc_file"; then
833
- sed -i.bak '/contains.*\.aidevops\/bin/d' "$rc_file"
834
- rm -f "${rc_file}.bak"
835
- fi
836
-
837
- return 0
838
- }
839
-
840
- # Configure SHELLCHECK_PATH and PATH shim in a single rc file.
841
- # Prints "added:<file>" or "already:<file>" to stdout.
842
- _shellcheck_wrapper_setup_one_rc() {
843
- local rc_file="$1"
844
- local env_line="$2"
845
- local path_line="$3"
846
- local env_line_fish="$4"
847
- local path_line_fish="$5"
848
-
849
- if [[ ! -f "$rc_file" ]]; then
850
- mkdir -p "$(dirname "$rc_file")"
851
- touch "$rc_file"
852
- fi
853
-
854
- # Detect fish config — uses set -gx syntax, not export
855
- local is_fish_rc=false
856
- [[ "$rc_file" == *"/fish/config.fish" ]] && is_fish_rc=true
857
-
858
- # Select the correct syntax for this shell
859
- local rc_env_line="$env_line"
860
- local rc_path_line="$path_line"
861
- if [[ "$is_fish_rc" == "true" ]]; then
862
- rc_env_line="$env_line_fish"
863
- rc_path_line="$path_line_fish"
864
- fi
865
-
866
- # SHELLCHECK_PATH env var
867
- if grep -q 'SHELLCHECK_PATH' "$rc_file"; then
868
- echo "already:$rc_file"
869
- else
870
- {
871
- echo ""
872
- echo "# Added by aidevops setup (GH#2915: prevent ShellCheck memory explosion)"
873
- echo "$rc_env_line"
874
- } >>"$rc_file"
875
- echo "added:$rc_file"
876
- fi
877
-
878
- # PATH prepend for ~/.aidevops/bin (GH#2993)
879
- _shellcheck_wrapper_remove_stale_path "$rc_file" "$is_fish_rc"
880
- # Check for the new sanitize-and-prepend form (uses _aidevops_shim variable)
881
- if ! grep -Fq '_aidevops_shim' "$rc_file"; then
882
- {
883
- echo ""
884
- echo "# Added by aidevops setup (GH#2993: shellcheck shim on PATH)"
885
- echo "$rc_path_line"
886
- } >>"$rc_file"
887
- fi
888
-
889
- return 0
890
- }
891
-
892
- # Layer 3: Configure SHELLCHECK_PATH and PATH shim in interactive shell rc files.
893
- _shellcheck_wrapper_setup_rc_files() {
894
- local env_line="$1"
895
- local path_line="$2"
896
- local env_line_fish="$3"
897
- local path_line_fish="$4"
898
- local added_to_out=""
899
- local already_in_out=""
900
-
901
- local rc_file
902
- while IFS= read -r rc_file; do
903
- [[ -z "$rc_file" ]] && continue
904
- local result
905
- result=$(_shellcheck_wrapper_setup_one_rc "$rc_file" "$env_line" "$path_line" "$env_line_fish" "$path_line_fish")
906
- case "$result" in
907
- added:*) added_to_out="${added_to_out:+$added_to_out, }${result#added:}" ;;
908
- already:*) already_in_out="${already_in_out:+$already_in_out, }${result#already:}" ;;
909
- esac
910
- done < <(get_all_shell_rcs)
911
-
912
- echo "$added_to_out|$already_in_out"
913
- return 0
914
- }
915
-
916
- # Merge results from zshenv and rc-file layers and print status messages.
917
- _shellcheck_wrapper_report_results() {
918
- local env_line="$1"
919
- local zshenv_result="$2"
920
- local rc_result="$3"
921
-
922
- local zshenv_added zshenv_already
923
- zshenv_added="${zshenv_result%% *}"
924
- zshenv_already="${zshenv_result##* }"
925
-
926
- local rc_added rc_already
927
- rc_added="${rc_result%%|*}"
928
- rc_already="${rc_result##*|}"
929
-
930
- # Merge results from layers 2 and 3
931
- local added_to=""
932
- local already_in=""
933
- [[ -n "$zshenv_added" && "$zshenv_added" != " " ]] && added_to="${zshenv_added}"
934
- [[ -n "$rc_added" ]] && added_to="${added_to:+$added_to, }${rc_added}"
935
- [[ -n "$zshenv_already" && "$zshenv_already" != " " ]] && already_in="${zshenv_already}"
936
- [[ -n "$rc_already" ]] && already_in="${already_in:+$already_in, }${rc_already}"
937
-
938
- if [[ -n "$added_to" ]]; then
939
- print_success "Configured SHELLCHECK_PATH wrapper in: $added_to"
940
- fi
941
-
942
- if [[ -n "$already_in" ]]; then
943
- print_info "SHELLCHECK_PATH already configured in: $already_in"
944
- fi
945
-
946
- if [[ -z "$added_to" && -z "$already_in" && "$PLATFORM_MACOS" != "true" ]]; then
947
- print_warning "Could not configure SHELLCHECK_PATH automatically"
948
- print_info "Add this to your shell config: $env_line"
949
- fi
950
-
951
- return 0
952
- }
953
-
954
- setup_shellcheck_wrapper() {
955
- local wrapper_path="$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"
956
-
957
- _shellcheck_wrapper_verify "$wrapper_path" || return 0
958
-
959
- local env_line
960
- # shellcheck disable=SC2016 # env_line is written to rc files; must expand at shell startup
961
- env_line='export SHELLCHECK_PATH="$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"'
962
- # shellcheck disable=SC2016 # path_line is written to rc files; must expand at shell startup
963
- # Sanitize-and-prepend: strip any existing occurrence of the shim dir from PATH
964
- # (it may be at the END from a previous setup run), then prepend it. This ensures
965
- # the shim is always first, even on machines upgrading from the old append form.
966
- # The ${PATH:+:$PATH} guard handles the empty-PATH edge case without a trailing colon.
967
- 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'
968
- # Fish shell uses different syntax (set -gx instead of export)
969
- # shellcheck disable=SC2016 # fish lines are written to config.fish; must expand at shell startup
970
- local env_line_fish='set -gx SHELLCHECK_PATH "$HOME/.aidevops/agents/scripts/shellcheck-wrapper.sh"'
971
- # shellcheck disable=SC2016 # fish path line: strip existing, then prepend
972
- 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'
973
-
974
- local shim_dir="$HOME/.aidevops/bin"
975
-
976
- # Layer 0: PATH shim (GH#2993)
977
- _shellcheck_wrapper_setup_shim "$wrapper_path"
978
-
979
- # Layer 1: launchctl setenv (macOS) — GUI-launched processes
980
- if [[ "$PLATFORM_MACOS" == "true" ]]; then
981
- _shellcheck_wrapper_setup_launchctl "$wrapper_path" "$shim_dir"
982
- fi
983
-
984
- # Layer 2: .zshenv — ALL zsh processes (interactive AND non-interactive)
985
- local zshenv_result
986
- zshenv_result=$(_shellcheck_wrapper_setup_zshenv "$env_line" "$path_line" "$shim_dir")
987
-
988
- # Layer 3: Shell rc files — interactive terminal sessions
989
- local rc_result
990
- rc_result=$(_shellcheck_wrapper_setup_rc_files "$env_line" "$path_line" "$env_line_fish" "$path_line_fish")
991
-
992
- # Report merged results from layers 2 and 3
993
- _shellcheck_wrapper_report_results "$env_line" "$zshenv_result" "$rc_result"
994
-
995
- # Also export for current session
996
- export SHELLCHECK_PATH="$wrapper_path"
997
- export PATH="$HOME/.aidevops/bin:$PATH"
998
-
999
- return 0
1000
- }
1001
-
1002
- # Check whether server access aliases are already configured in any rc file.
1003
- # Returns 0 (true) if already configured, 1 (false) if not.
1004
- _aliases_check_configured() {
1005
- local rc_file
1006
- while IFS= read -r rc_file; do
1007
- [[ -z "$rc_file" ]] && continue
1008
- if [[ -f "$rc_file" ]] && grep -q "# AI Assistant Server Access" "$rc_file"; then
1009
- return 0
1010
- fi
1011
- done < <(get_all_shell_rcs)
1012
-
1013
- # Also check fish config (not included in get_all_shell_rcs on macOS)
1014
- local fish_config="$HOME/.config/fish/config.fish"
1015
- if [[ -f "$fish_config" ]] && grep -q "# AI Assistant Server Access" "$fish_config"; then
1016
- return 0
1017
- fi
1018
-
1019
- return 1
1020
- }
1021
-
1022
- # Replace the aidevops alias block in a single rc file.
1023
- # Removes the old block (marker line through the next blank line) and appends
1024
- # the new block. Idempotent: safe to call on every setup run.
1025
- _aliases_replace_in_file() {
1026
- local rc_file="$1"
1027
- local alias_block="$2"
1028
- local marker="# AI Assistant Server Access"
1029
- local tmp_file
1030
- tmp_file=$(mktemp "${TMPDIR:-/tmp}/aidevops-aliases-XXXXXX") || return 1
1031
-
1032
- # Strip existing block: from marker line to the next blank line (inclusive)
1033
- awk -v marker="$marker" '
1034
- $0 ~ marker { in_block=1; next }
1035
- in_block && /^[[:space:]]*$/ { in_block=0; next }
1036
- in_block { next }
1037
- { print }
1038
- ' "$rc_file" >"$tmp_file" && mv "$tmp_file" "$rc_file"
1039
-
1040
- # Append updated block
1041
- printf '%s\n' "$alias_block" >>"$rc_file"
1042
- return 0
1043
- }
1044
-
1045
- # Write alias blocks to fish or bash/zsh rc files.
1046
- # Always replaces an existing aidevops alias block so updates propagate.
1047
- # Prints the list of files aliases were added/updated in (comma-separated).
1048
- _aliases_write_to_rc_files() {
1049
- local is_fish="$1"
1050
- local alias_block_bash="$2"
1051
- local alias_block_fish="$3"
1052
- local added_to=""
1053
-
1054
- # Handle fish separately
1055
- if [[ "$is_fish" == "true" ]]; then
1056
- local fish_rc="$HOME/.config/fish/config.fish"
1057
- mkdir -p "$HOME/.config/fish"
1058
- if [[ ! -f "$fish_rc" ]]; then
1059
- touch "$fish_rc"
1060
- fi
1061
- _aliases_replace_in_file "$fish_rc" "$alias_block_fish"
1062
- added_to="$fish_rc"
1063
- else
1064
- # Add/update in all bash/zsh rc files
1065
- local rc_file
1066
- while IFS= read -r rc_file; do
1067
- [[ -z "$rc_file" ]] && continue
1068
-
1069
- # Create if it doesn't exist
1070
- if [[ ! -f "$rc_file" ]]; then
1071
- touch "$rc_file"
1072
- fi
1073
-
1074
- _aliases_replace_in_file "$rc_file" "$alias_block_bash"
1075
- added_to="${added_to:+$added_to, }$rc_file"
1076
- done < <(get_all_shell_rcs)
1077
- fi
1078
-
1079
- echo "$added_to"
1080
- return 0
1081
- }
1082
-
1083
- # Build the bash/zsh and fish alias blocks for server access.
1084
- # Prints two lines: "bash:<block>" and "fish:<block>" — callers split on newline.
1085
- # Since blocks are multi-line, we use process substitution via temp files.
1086
- # Outputs bash block to stdout, fish block to fd3 (caller must set up fd3).
1087
- _aliases_build_blocks() {
1088
- cat <<'ALIASES'
1089
-
1090
- # AI Assistant Server Access Framework
1091
- alias servers='./.agents/scripts/servers-helper.sh'
1092
- alias servers-list='./.agents/scripts/servers-helper.sh list'
1093
- alias hostinger='./.agents/scripts/hostinger-helper.sh'
1094
- alias hetzner='./.agents/scripts/hetzner-helper.sh'
1095
- alias aws-helper='./.agents/scripts/aws-helper.sh'
1096
- # Claude Code: skip interactive permission prompts (safe for personal machines)
1097
- command -v claude >/dev/null 2>&1 && alias claude='claude --dangerously-skip-permissions'
1098
- ALIASES
1099
- return 0
1100
- }
1101
-
1102
- # Build the fish-syntax alias block for server access.
1103
- _aliases_build_fish_block() {
1104
- cat <<'ALIASES'
1105
-
1106
- # AI Assistant Server Access Framework
1107
- alias servers './.agents/scripts/servers-helper.sh'
1108
- alias servers-list './.agents/scripts/servers-helper.sh list'
1109
- alias hostinger './.agents/scripts/hostinger-helper.sh'
1110
- alias hetzner './.agents/scripts/hetzner-helper.sh'
1111
- alias aws-helper './.agents/scripts/aws-helper.sh'
1112
- # Claude Code: skip interactive permission prompts (safe for personal machines)
1113
- if command -v claude >/dev/null 2>&1; alias claude 'claude --dangerously-skip-permissions'; end
1114
- ALIASES
1115
- return 0
1116
- }
1117
-
1118
- # Add server access aliases to shell rc files (bash/zsh/fish)
1119
- setup_aliases() {
1120
- print_info "Setting up shell aliases..."
1121
-
1122
- local default_shell
1123
- default_shell=$(detect_default_shell)
1124
-
1125
- # Fish shell uses different alias syntax
1126
- local is_fish=false
1127
- [[ "$default_shell" == "fish" ]] && is_fish=true
1128
-
1129
- local alias_block_bash
1130
- alias_block_bash=$(_aliases_build_blocks)
1131
-
1132
- local alias_block_fish
1133
- alias_block_fish=$(_aliases_build_fish_block)
1134
-
1135
- print_info "Detected default shell: $default_shell"
1136
- setup_prompt add_aliases "Add shell aliases? [Y/n]: " "Y"
1137
-
1138
- if [[ "$add_aliases" =~ ^[Yy]?$ ]]; then
1139
- local added_to
1140
- added_to=$(_aliases_write_to_rc_files "$is_fish" "$alias_block_bash" "$alias_block_fish")
1141
-
1142
- if [[ -n "$added_to" ]]; then
1143
- print_success "Aliases added to: $added_to"
1144
- print_info "Restart your terminal to use aliases"
1145
- fi
1146
- else
1147
- print_info "Skipped alias setup by user request"
1148
- fi
1149
- return 0
1150
- }
1151
-
1152
- # Check if terminal title integration is already configured in any rc file.
1153
- # Returns 0 if configured, 1 if not.
1154
- _terminal_title_is_configured() {
1155
- local rc_file
1156
- while IFS= read -r rc_file; do
1157
- [[ -z "$rc_file" ]] && continue
1158
- if [[ -f "$rc_file" ]] && grep -q "aidevops terminal-title" "$rc_file"; then
1159
- return 0
1160
- fi
1161
- done < <(get_all_shell_rcs)
1162
- return 1
1163
- }
1164
-
1165
- # Print current shell and Tabby status for terminal title setup.
1166
- _terminal_title_show_status() {
1167
- echo ""
1168
- print_info "Terminal title integration syncs your terminal tab with git repo/branch"
1169
- print_info "Example: Tab shows 'aidevops/feature/xyz' when in that branch"
1170
- echo ""
1171
- echo "Current status:"
1172
-
1173
- local shell_name
1174
- shell_name=$(detect_default_shell)
1175
- local shell_info="$shell_name"
1176
- if [[ "$shell_name" == "zsh" ]] && [[ -d "$HOME/.oh-my-zsh" ]]; then
1177
- shell_info="$shell_name (Oh-My-Zsh)"
1178
- fi
1179
- echo " Shell: $shell_info"
1180
-
1181
- local tabby_config="$HOME/Library/Application Support/tabby/config.yaml"
1182
- if [[ -f "$tabby_config" ]]; then
1183
- local disabled_count
1184
- # grep -c exits 1 on no match; || : inside subshell prevents ERR trap noise
1185
- disabled_count=$(grep -c "disableDynamicTitle: true" "$tabby_config" || :)
1186
- if [[ "${disabled_count:-0}" -gt 0 ]]; then
1187
- echo " Tabby: detected, dynamic titles disabled in $disabled_count profile(s) (will fix)"
1188
- else
1189
- echo " Tabby: detected, dynamic titles enabled"
1190
- fi
1191
- fi
1192
-
1193
- return 0
1194
- }
1195
-
1196
- # Prompt the user and run the terminal title installer.
1197
- _terminal_title_prompt_and_install() {
1198
- local setup_script="$1"
1199
-
1200
- echo ""
1201
- setup_prompt install_title "Install terminal title integration? [Y/n]: " "Y"
1202
-
1203
- if [[ "$install_title" =~ ^[Yy]?$ ]]; then
1204
- if bash "$setup_script" install; then
1205
- print_success "Terminal title integration installed"
1206
- else
1207
- print_warning "Terminal title setup encountered issues (non-critical)"
1208
- fi
1209
- else
1210
- print_info "Skipped terminal title setup by user request"
1211
- print_info "You can install later with: ~/.aidevops/agents/scripts/terminal-title-setup.sh install"
1212
- fi
1213
-
1214
- return 0
1215
- }
1216
-
1217
- # Install terminal title integration that syncs tab titles with git repo/branch
1218
- setup_terminal_title() {
1219
- # Check prerequisites before announcing setup (GH#5240)
1220
- local setup_script=".agents/scripts/terminal-title-setup.sh"
1221
-
1222
- if [[ ! -f "$setup_script" ]]; then
1223
- print_skip "Terminal title" "setup script not found" "Deploy agents first (setup.sh), then re-run"
1224
- setup_track_skipped "Terminal title" "setup script not found"
1225
- return 0
1226
- fi
1227
-
1228
- if _terminal_title_is_configured; then
1229
- print_success "Terminal title integration already configured"
1230
- setup_track_configured "Terminal title"
1231
- return 0
1232
- fi
1233
-
1234
- # Prerequisites met — proceed with setup
1235
- print_info "Setting up terminal title integration..."
1236
- _terminal_title_show_status
1237
- _terminal_title_prompt_and_install "$setup_script"
1238
-
1239
- return 0
1240
- }