#!/usr/bin/env bash ## ## ## ## ## : ========================================== : Your Team Config Sync : Unified AWS + SSH Setup : ========================================== # This script configures AWS CLI and SSH on your MacBook for Your Team development. # Run it with: # : curl -fsSL config.yourteam.example | bash # # Prerequisites: # - GitHub account with access to your-org/config-sync # - 1Password desktop app installed # - 1Password CLI integration enabled # (Settings → Developer → Integrate with 1Password CLI) # - Full Disk Access granted to Terminal/iTerm # (System Settings → Privacy & Security → Full Disk Access) # # What this installs: # - Homebrew, AWS CLI, jq, gum, gh, 1Password CLI # - AWS credential helper (fetches credentials from 1Password) # - SSH configuration with 1Password SSH agent integration # - Menu bar app for sync status and manual sync # # Source code: # https://github.com/your-org/config-sync # : ========================================== : Script Start : ========================================== set -euo pipefail # Prevent Homebrew from auto-updating on each install (speeds up script significantly) export HOMEBREW_NO_AUTO_UPDATE=1 # ------------------------------------------ # Logging Setup # ------------------------------------------ # All brew/compile output goes to log file for debugging LOG_DIR="$HOME/.yourteam/logs" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/setup-$(date +%Y%m%d-%H%M%S).log" touch "$LOG_FILE" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE" } # Trap unexpected errors and show log location trap 'echo ""; echo " Setup failed unexpectedly. Check log: $LOG_FILE"; log "Script failed at line $LINENO"' ERR log "Setup script started" log "Running on: $(uname -a)" # Bootstrap: GitHub repo coordinates (private repo - uses gh api) # Everything else is derived from config.json GH_OWNER="your-org" GH_REPO="config-sync" GH_PATH_PREFIX="public" # These will be populated from config.json after jq is installed ORG_NAME="" APP_NAME="" APP_NAME_SHORT="" BUNDLE_ID="" DOMAIN="" LOCAL_DIR="" GITHUB_OWNER="" GITHUB_REPO="" OP_ACCOUNT_VALUE="" CONFIG_DIR="" # ------------------------------------------ # 80s Retro Synthwave Color Palette # ------------------------------------------ # # Accessibility notes: # - NO_COLOR env var disables all colors (standard convention) # - TERM=dumb disables colors for basic terminals # - Colors chosen for high contrast (WCAG AA compliant on dark backgrounds) # - Neon colors provide 4.5:1+ contrast ratio against dark backgrounds # - All text colors tested against #1a1a2e and #0d0d1a backgrounds # Primary: Hot Pink / Magenta PINK="212" # ANSI 256: bright pink (close to #ff87d7) HEX_PINK="#ff71ce" # Neon hot pink HEX_PINK_SOFT="#f2a1f2" # Softer pink (user specified) # Secondary: Electric Blue / Purple PURPLE="99" # ANSI 256: medium purple BLUE="63" # ANSI 256: slate blue HEX_PURPLE="#b967ff" # Neon purple HEX_BLUE="#707ff5" # Electric blue (user specified) # Accent: Cyan / Aqua CYAN="51" # ANSI 256: cyan HEX_CYAN="#01cdfe" # Electric cyan/aqua # Success: Neon Green GREEN="48" # ANSI 256: spring green HEX_GREEN="#05ffa1" # Neon mint green # Warning: Neon Yellow YELLOW="227" # ANSI 256: light yellow HEX_YELLOW="#fffb96" # Neon yellow # Error: Hot Red/Orange RED="196" # ANSI 256: red HEX_RED="#fe4a49" # Neon red-orange # Neutral: Cool grays GRAY="245" # ANSI 256: gray WHITE="255" # ANSI 256: white HEX_GRAY="#6c6783" # Muted purple-gray HEX_WHITE="#f0e6ff" # Warm white with slight purple tint # Background: Deep dark with purple undertone HEX_BG_DARK="#0d0d1a" # Deepest background HEX_BG="#1a1a2e" # Primary dark background HEX_BG_LIGHT="#16213e" # Lighter panel background # ANSI escape sequences (fallback for non-gum environments) ANSI_PINK='\033[38;5;212m' ANSI_PURPLE='\033[38;5;99m' ANSI_BLUE='\033[38;5;63m' ANSI_CYAN='\033[38;5;51m' ANSI_GREEN='\033[38;5;48m' ANSI_YELLOW='\033[38;5;227m' ANSI_RED='\033[38;5;196m' ANSI_GRAY='\033[38;5;245m' ANSI_BOLD='\033[1m' ANSI_DIM='\033[2m' ANSI_ITALIC='\033[3m' ANSI_RESET='\033[0m' # ------------------------------------------ # Accessibility & Environment Detection # ------------------------------------------ # Detect if colors should be disabled # NO_COLOR is a standard convention: https://no-color.org/ USE_COLOR=true if [[ -n "${NO_COLOR:-}" ]] || [[ "${TERM:-}" == "dumb" ]]; then USE_COLOR=false fi # Escape special characters for sed replacement strings # Escapes: & \ | (our delimiter) and newlines sed_escape() { printf '%s' "$1" | sed -e 's/[&/\|]/\\&/g' } # Progress tracking (used in step headers) CURRENT_STEP=0 TOTAL_STEPS=7 # ------------------------------------------ # Display Functions (with gum fallback) # ------------------------------------------ # Check if gum is available has_gum() { command -v gum &> /dev/null } # Display the ASCII art logo with synthwave styling show_logo() { echo "" # Main logo block if has_gum && [[ "$USE_COLOR" == "true" ]]; then local logo logo=$(gum style \ --foreground "$HEX_PINK" \ --background "$HEX_BG" \ --border double \ --border-foreground "$HEX_PURPLE" \ --padding "1 3" \ --width 50 \ --bold \ "██╗ ██╗ ██████╗ ██╗ ██╗██████╗ " \ "╚██╗ ██╔╝██╔═══██╗██║ ██║██╔══██╗" \ " ╚████╔╝ ██║ ██║██║ ██║██████╔╝" \ " ╚██╔╝ ██║ ██║██║ ██║██╔══██╗" \ " ██║ ╚██████╔╝╚██████╔╝██║ ██║" \ " ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝" \ "████████╗███████╗ █████╗ ███╗ ███╗" \ "╚══██╔══╝██╔════╝██╔══██╗████╗ ████║" \ " ██║ █████╗ ███████║██╔████╔██║" \ " ██║ ██╔══╝ ██╔══██║██║╚██╔╝██║" \ " ██║ ███████╗██║ ██║██║ ╚═╝ ██║" \ " ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝") local tagline tagline=$(gum style \ --foreground "$HEX_CYAN" \ --italic \ --padding "0 2" \ "MacBook Developer Config Tool") gum join --vertical --align center "$logo" "$tagline" elif [[ "$USE_COLOR" == "true" ]]; then echo -e "${ANSI_PINK}${ANSI_BOLD}" echo " ╔══════════════════════════════════════════════════╗" echo " ║ ██╗ ██╗ ██████╗ ██╗ ██╗██████╗ ║" echo " ║ ╚██╗ ██╔╝██╔═══██╗██║ ██║██╔══██╗ ║" echo " ║ ╚████╔╝ ██║ ██║██║ ██║██████╔╝ ║" echo " ║ ╚██╔╝ ██║ ██║██║ ██║██╔══██╗ ║" echo " ║ ██║ ╚██████╔╝╚██████╔╝██║ ██║ ║" echo " ║ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ║" echo " ║ ████████╗███████╗ █████╗ ███╗ ███╗ ║" echo " ║ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║ ║" echo " ║ ██║ █████╗ ███████║██╔████╔██║ ║" echo " ║ ██║ ██╔══╝ ██╔══██║██║╚██╔╝██║ ║" echo " ║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║ ║" echo " ║ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ║" echo " ╚══════════════════════════════════════════════════╝" echo -e "${ANSI_RESET}" echo -e " ${ANSI_CYAN}${ANSI_ITALIC}MacBook Developer Config Tool${ANSI_RESET}" else # NO_COLOR mode - plain text only echo " ╔══════════════════════════════════════════════════╗" echo " ║ Your Team Config Sync ║" echo " ╚══════════════════════════════════════════════════╝" echo " MacBook Developer Config Tool" fi echo "" } # Display a step header with progress indicator show_step() { local title="$1" ((CURRENT_STEP++)) echo "" if has_gum && [[ "$USE_COLOR" == "true" ]]; then # Create step badge with synthwave purple background local badge badge=$(gum style \ --foreground "$HEX_WHITE" \ --background "$HEX_PURPLE" \ --padding "0 1" \ --bold \ "STEP $CURRENT_STEP/$TOTAL_STEPS") # Create title with pink accent local step_title step_title=$(gum style \ --foreground "$HEX_PINK" \ --bold \ --padding "0 1" \ "$title") # Join horizontally gum join --horizontal --align center "$badge" "$step_title" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_PURPLE}${ANSI_BOLD}[$CURRENT_STEP/$TOTAL_STEPS]${ANSI_RESET} ${ANSI_PINK}${ANSI_BOLD}$title${ANSI_RESET}" else echo " [$CURRENT_STEP/$TOTAL_STEPS] $title" fi } # Display a section header (smaller than step, for sub-sections within a phase) show_section() { local message="$1" echo "" if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum style \ --border rounded \ --border-foreground "$HEX_BLUE" \ --padding "0 2" \ --margin "0 1" \ --foreground "$HEX_WHITE" \ "$message" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_BLUE}╭─────────────────────────────────────────╮${ANSI_RESET}" echo -e " ${ANSI_BLUE}│${ANSI_RESET} $message" echo -e " ${ANSI_BLUE}╰─────────────────────────────────────────╯${ANSI_RESET}" else echo " --- $message ---" fi } # Display a success message with checkmark (neon green) show_success() { local message="$1" if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum style --foreground "$HEX_GREEN" --margin "0 1" "✓ $message" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_GREEN}✓ $message${ANSI_RESET}" else echo " [OK] $message" fi } # Display an info message (muted purple-gray) show_info() { local message="$1" if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum style --foreground "$HEX_GRAY" --margin "0 1" --faint " $message" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_GRAY}$message${ANSI_RESET}" else echo " $message" fi } # Display a warning message (neon yellow) show_warning() { local message="$1" if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum style --foreground "$HEX_YELLOW" --margin "0 1" "⚠ $message" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_YELLOW}⚠ $message${ANSI_RESET}" else echo " [WARN] $message" fi } # Display an error message (hot red) show_error() { local message="$1" if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum style --foreground "$HEX_RED" --margin "0 1" --bold "✗ $message" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_RED}✗ $message${ANSI_RESET}" else echo " [ERROR] $message" fi } # Run a command with animated spinner spin_process() { local title="$1" shift if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum spin --spinner dot --spinner.foreground "$HEX_PINK" --title "$title" -- "$@" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_PINK}•${ANSI_RESET} $title" "$@" else echo " [...] $title" "$@" fi } # Display an error box with prominent border show_error_box() { echo "" if has_gum && [[ "$USE_COLOR" == "true" ]]; then gum style \ --border double \ --border-foreground "$HEX_RED" \ --foreground "$HEX_RED" \ --padding "1 2" \ --margin "0 1" \ --bold \ "$@" elif [[ "$USE_COLOR" == "true" ]]; then echo -e " ${ANSI_RED}╔═════════════════════════════════════════════════╗${ANSI_RESET}" for line in "$@"; do echo -e " ${ANSI_RED}║${ANSI_RESET} $line" done echo -e " ${ANSI_RED}╚═════════════════════════════════════════════════╝${ANSI_RESET}" else echo "" echo " ==========================================" for line in "$@"; do echo " $line" done echo " ==========================================" fi } # Display completion banner with summary # Note: This function uses ORG_NAME which is loaded from config.json show_completion() { # Use ORG_NAME if loaded, otherwise fallback local org="${ORG_NAME:-Your Team}" echo "" if has_gum && [[ "$USE_COLOR" == "true" ]]; then # Inner content width (44 + 6 padding + 2 border = 52, matching logo) local w=44 # Create the celebration header with neon green on dark background local header header=$(gum style \ --foreground "$HEX_GREEN" \ --background "$HEX_BG" \ --bold \ --align center \ --width $w \ "✓ Setup Complete!") # Create info sections with 2-char left indent for readability local info info=$(gum style \ --foreground "$HEX_WHITE" \ --background "$HEX_BG" \ --width $w \ --padding "1 0" \ " Look for the $org icon in your menu bar." \ "" \ "$(gum style --foreground "$HEX_PINK" --background "$HEX_BG" --width $w --bold " Use your $org profile:")" \ "$(gum style --foreground "$HEX_WHITE" --background "$HEX_BG" --width $w " aws s3 ls")" \ "" \ "$(gum style --foreground "$HEX_PINK" --background "$HEX_BG" --width $w --bold " Use a client profile:")" \ "$(gum style --foreground "$HEX_WHITE" --background "$HEX_BG" --width $w " aws s3 ls --profile ")") # Join header and info, wrap in box matching logo style local content content=$(gum join --vertical --align center "$header" "$info") gum style \ --background "$HEX_BG" \ --border double \ --border-foreground "$HEX_PURPLE" \ --padding "1 3" \ "$content" elif [[ "$USE_COLOR" == "true" ]]; then echo -e "${ANSI_PURPLE}╔══════════════════════════════════════════════════╗${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_GREEN}${ANSI_BOLD}✓ Setup Complete!${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} Look for the $org icon in your menu bar. ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PINK}Use your $org profile:${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} aws s3 ls ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PINK}Use a client profile:${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} aws s3 ls --profile ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}║${ANSI_RESET} ${ANSI_PURPLE}║${ANSI_RESET}" echo -e "${ANSI_PURPLE}╚══════════════════════════════════════════════════╝${ANSI_RESET}" else echo "" echo "╔══════════════════════════════════════════════════╗" echo "║ ║" echo "║ ✓ Setup Complete! ║" echo "║ ║" echo "║ Look for the $org icon in your menu bar. ║" echo "║ ║" echo "║ Use your $org profile: ║" echo "║ aws s3 ls ║" echo "║ ║" echo "║ Use a client profile: ║" echo "║ aws s3 ls --profile ║" echo "║ ║" echo "╚══════════════════════════════════════════════════╝" fi echo "" } : ------------------------------------------ : Show Logo : ------------------------------------------ show_logo sleep 0.5 log "Log file: $LOG_FILE" : ------------------------------------------ : Phase 1: Install Homebrew : ------------------------------------------ show_step "Installing Homebrew" if ! command -v brew &> /dev/null; then log "Homebrew not found, installing..." show_info "Homebrew not found, installing..." # Use /dev/tty for interactive input when running via curl | bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" > "$SHELL_PROFILE" elif [[ -f /usr/local/bin/brew ]]; then echo 'eval "$(/usr/local/bin/brew shellenv)"' >> "$SHELL_PROFILE" fi fi show_success "Homebrew installed" else show_success "Homebrew already installed" fi : ------------------------------------------ : Phase 2: Install Gum : ------------------------------------------ show_step "Installing Gum" if ! has_gum; then log "Installing Gum..." spin_process "Installing Gum..." bash -c "brew install gum < /dev/null >> '$LOG_FILE' 2>&1" if ! has_gum; then show_error "Gum installation failed" show_info "Check log file: $LOG_FILE" exit 1 fi show_success "Installed Gum" else show_success "Gum already installed" fi : ------------------------------------------ : Phase 3: Install CLI tools : ------------------------------------------ show_step "Installing CLI tools" log "Installing AWS CLI..." spin_process "Installing AWS CLI..." bash -c "brew install awscli < /dev/null >> '$LOG_FILE' 2>&1" if ! command -v aws &>/dev/null; then show_error "AWS CLI installation failed" show_info "Check log file: $LOG_FILE" exit 1 fi show_success "Installed AWS CLI" log "Installing jq..." spin_process "Installing jq..." bash -c "brew install jq < /dev/null >> '$LOG_FILE' 2>&1" if ! command -v jq &>/dev/null; then show_error "jq installation failed" show_info "Check log file: $LOG_FILE" exit 1 fi show_success "Installed jq" log "Installing GitHub CLI..." spin_process "Installing GitHub CLI..." bash -c "brew install gh < /dev/null >> '$LOG_FILE' 2>&1" if ! command -v gh &>/dev/null; then show_error "GitHub CLI installation failed" show_info "Check log file: $LOG_FILE" exit 1 fi show_success "Installed GitHub CLI" # ------------------------------------------ # Verify GitHub CLI authentication # ------------------------------------------ # The config files are in a private repo, so we need gh authenticated GH_AUTH_OK=false spin_process "Verifying GitHub authentication..." bash -c "gh auth status &>/dev/null && exit 0 || exit 1" && GH_AUTH_OK=true if [[ "$GH_AUTH_OK" != "true" ]]; then show_warning "GitHub authentication required" show_info "This setup requires access to the private config repository." echo "" # Check if we have a TTY for interactive login if [[ -t 0 ]] || [[ -e /dev/tty ]]; then show_info "Please authenticate with your GitHub account..." echo "" # Use /dev/tty to get interactive input when running via curl | bash if ! gh auth login /dev/null" && REPO_ACCESS_OK=true if [[ "$REPO_ACCESS_OK" != "true" ]]; then show_error_box \ "Cannot access repository" \ "" \ "Your GitHub account doesn't have access to:" \ " $GH_OWNER/$GH_REPO" \ "" \ "Please contact your admin for repository access." exit 1 fi show_success "Repository access confirmed" # ------------------------------------------ # Load configuration from config.json # ------------------------------------------ # Now that jq is available, fetch and parse the central config file # This allows the entire setup to be customized by editing config.json CONFIG_TMP=$(mktemp) spin_process "Loading configuration..." bash -c "gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/config.json -H 'Accept: application/vnd.github.raw' > '$CONFIG_TMP' 2>/dev/null" if [[ ! -s "$CONFIG_TMP" ]]; then rm -f "$CONFIG_TMP" show_error "Failed to fetch config.json from $GH_OWNER/$GH_REPO" exit 1 fi CONFIG_JSON=$(cat "$CONFIG_TMP") rm -f "$CONFIG_TMP" # Validate JSON is parseable if ! echo "$CONFIG_JSON" | jq empty 2>/dev/null; then show_error "Invalid JSON in config.json" exit 1 fi # Extract branding values ORG_NAME=$(echo "$CONFIG_JSON" | jq -r '.branding.orgName // empty') APP_NAME=$(echo "$CONFIG_JSON" | jq -r '.branding.appName // empty') APP_NAME_SHORT=$(echo "$CONFIG_JSON" | jq -r '.branding.appNameShort // empty') APP_EXECUTABLE=$(echo "$APP_NAME_SHORT" | tr -d ' ') # "Config Sync" → "ConfigSync" BUNDLE_ID=$(echo "$CONFIG_JSON" | jq -r '.branding.bundleId // empty') DOMAIN=$(echo "$CONFIG_JSON" | jq -r '.branding.domain // empty') LOCAL_DIR=$(echo "$CONFIG_JSON" | jq -r '.branding.localDir // empty') # Extract GitHub values GITHUB_OWNER=$(echo "$CONFIG_JSON" | jq -r '.github.owner // empty') GITHUB_REPO=$(echo "$CONFIG_JSON" | jq -r '.github.repo // empty') # Extract 1Password values OP_ACCOUNT_VALUE=$(echo "$CONFIG_JSON" | jq -r '.onepassword.account // empty') # Validate required fields for var in ORG_NAME APP_NAME BUNDLE_ID DOMAIN LOCAL_DIR GITHUB_OWNER GITHUB_REPO OP_ACCOUNT_VALUE; do if [[ -z "${!var}" ]]; then show_error "Missing required config value: $var" show_info "Please check your config.json file" exit 1 fi done # Set CONFIG_DIR based on loaded config CONFIG_DIR="$HOME/$LOCAL_DIR" show_success "Loaded configuration" log "Installing 1Password CLI..." spin_process "Installing 1Password CLI..." bash -c "brew install --cask 1password-cli < /dev/null >> '$LOG_FILE' 2>&1" if ! command -v op &>/dev/null; then show_error "1Password CLI installation failed" show_info "Check log file: $LOG_FILE" exit 1 fi show_success "Installed 1Password CLI" : ------------------------------------------ : Phase 4: Verify 1Password CLI : ------------------------------------------ show_step "Verifying 1Password" OP_CLI_OK=false spin_process "Checking 1Password CLI..." bash -c "op account list &>/dev/null" && OP_CLI_OK=true if [[ "$OP_CLI_OK" != "true" ]]; then show_error_box \ "1Password CLI needs to be connected" \ "" \ "Please complete these steps:" \ "" \ " 1. Open 1Password app" \ " 2. Go to Settings → Developer" \ " 3. Enable 'Integrate with 1Password CLI'" \ "" \ "After enabling, re-run this script." exit 1 fi show_success "1Password CLI connected" : ------------------------------------------ : Phase 5: Configure environment : ------------------------------------------ show_step "Configuring environment" # Create directory structure (frontloaded for all subsequent phases) # SSH dirs (~/.ssh/config.d, ~/.config/1Password/ssh) are managed by the Swift app's SSHModule log "Creating directory structure..." mkdir -p "$CONFIG_DIR"/{app,config,logs} mkdir -p "$HOME/.aws" show_success "Created directory structure" # Determine shell profile (create .zshrc if none exists) SHELL_PROFILE="" if [[ -f "$HOME/.zshrc" ]]; then SHELL_PROFILE="$HOME/.zshrc" elif [[ -f "$HOME/.bashrc" ]]; then SHELL_PROFILE="$HOME/.bashrc" elif [[ -f "$HOME/.bash_profile" ]]; then SHELL_PROFILE="$HOME/.bash_profile" else SHELL_PROFILE="$HOME/.zshrc" touch "$SHELL_PROFILE" fi if ! grep -q "OP_ACCOUNT" "$SHELL_PROFILE" 2>/dev/null; then log "Configuring OP_ACCOUNT in $SHELL_PROFILE" echo '' >> "$SHELL_PROFILE" echo "# 1Password account for credential helpers ($ORG_NAME)" >> "$SHELL_PROFILE" echo "export OP_ACCOUNT=\"$OP_ACCOUNT_VALUE\"" >> "$SHELL_PROFILE" export OP_ACCOUNT="$OP_ACCOUNT_VALUE" show_success "OP_ACCOUNT configured" else show_success "OP_ACCOUNT already configured" fi # Ensure PATH includes app dir if ! grep -q "$LOCAL_DIR/app" "$SHELL_PROFILE" 2>/dev/null; then log "Adding $LOCAL_DIR/app to PATH in $SHELL_PROFILE" echo '' >> "$SHELL_PROFILE" echo "# $ORG_NAME tools" >> "$SHELL_PROFILE" echo "export PATH=\"\$HOME/$LOCAL_DIR/app:\$PATH\"" >> "$SHELL_PROFILE" show_success "PATH updated" else show_success "PATH already configured" fi : ------------------------------------------ : Phase 6: Build Swift applications : ------------------------------------------ show_step "Building native apps" APP_DIR="$CONFIG_DIR/app" show_section "Credential Helper" # Download and compile AWS credential helper log "Downloading credential helper..." spin_process "Downloading credential helper..." \ bash -c "gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Helpers/aws-vault-1password.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/aws-vault-1password.swift'" show_success "Downloaded credential helper" # Substitute branding values in Swift source before compiling # Use sed_escape to handle special characters in config values sed -i.bak "s|your-team.1password.com|$(sed_escape "$OP_ACCOUNT_VALUE")|g" "$APP_DIR/aws-vault-1password.swift" rm -f "$APP_DIR/aws-vault-1password.swift.bak" log "Compiling credential helper..." spin_process "Compiling credential helper..." \ bash -c "swiftc -O -o '$APP_DIR/aws-vault-1password' '$APP_DIR/aws-vault-1password.swift' >> '$LOG_FILE' 2>&1" if [[ ! -f "$APP_DIR/aws-vault-1password" ]]; then show_error "Credential helper compilation failed" show_info "Check log file: $LOG_FILE" exit 1 fi rm -f "$APP_DIR/aws-vault-1password.swift" chmod +x "$APP_DIR/aws-vault-1password" show_success "Compiled credential helper" show_section "Menu Bar App" # Download menu bar app source files log "Downloading menu bar app..." spin_process "Downloading menu bar app..." \ bash -c " mkdir -p '$APP_DIR/src/Core' '$APP_DIR/src/Modules' # Download Core modules gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/Config.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/Config.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/GitHubClient.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/GitHubClient.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/TOMLParser.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/TOMLParser.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/Preferences.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/Preferences.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/NetworkMonitor.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/NetworkMonitor.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/NotificationManager.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/NotificationManager.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/OnePasswordCLI.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/OnePasswordCLI.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/SyncModule.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/SyncModule.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Core/UpdateManager.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Core/UpdateManager.swift' # Download Module implementations gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Modules/AWSModule.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Modules/AWSModule.swift' gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/Modules/SSHModule.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/Modules/SSHModule.swift' # Download main app gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/app/ConfigSync.swift -H 'Accept: application/vnd.github.raw' > '$APP_DIR/src/ConfigSync.swift' " show_success "Downloaded menu bar app" # Substitute branding values in all Swift files # Order matters: replace longer/more specific strings first # Use sed_escape to handle special characters in config values log "Applying branding substitutions to Swift source files..." for swift_file in "$APP_DIR"/src/*.swift "$APP_DIR"/src/Core/*.swift "$APP_DIR"/src/Modules/*.swift; do if [[ -f "$swift_file" ]]; then sed -i.bak "s|config.yourteam.example|$(sed_escape "$DOMAIN")|g" "$swift_file" sed -i.bak "s|your-org/config-sync|$(sed_escape "$GITHUB_OWNER/$GITHUB_REPO")|g" "$swift_file" sed -i.bak "s|your-team.1password.com|$(sed_escape "$OP_ACCOUNT_VALUE")|g" "$swift_file" sed -i.bak "s|Your Team Config Sync|$(sed_escape "$APP_NAME")|g" "$swift_file" sed -i.bak "s|com.yourteam.config-sync|$(sed_escape "$BUNDLE_ID")|g" "$swift_file" sed -i.bak "s|\.yourteam|$(sed_escape "$LOCAL_DIR")|g" "$swift_file" sed -i.bak "s|Your Team|$(sed_escape "$ORG_NAME")|g" "$swift_file" rm -f "$swift_file.bak" fi done show_success "Applied branding configuration" log "Compiling menu bar app..." spin_process "Compiling menu bar app..." \ bash -c "swiftc -O -o '$APP_DIR/$APP_EXECUTABLE' \ '$APP_DIR/src/Core/Config.swift' \ '$APP_DIR/src/Core/GitHubClient.swift' \ '$APP_DIR/src/Core/TOMLParser.swift' \ '$APP_DIR/src/Core/Preferences.swift' \ '$APP_DIR/src/Core/NetworkMonitor.swift' \ '$APP_DIR/src/Core/NotificationManager.swift' \ '$APP_DIR/src/Core/OnePasswordCLI.swift' \ '$APP_DIR/src/Core/SyncModule.swift' \ '$APP_DIR/src/Core/UpdateManager.swift' \ '$APP_DIR/src/Modules/AWSModule.swift' \ '$APP_DIR/src/Modules/SSHModule.swift' \ '$APP_DIR/src/ConfigSync.swift' \ >> '$LOG_FILE' 2>&1" if [[ ! -f "$APP_DIR/$APP_EXECUTABLE" ]]; then show_error "Menu bar app compilation failed" show_info "Check log file: $LOG_FILE" exit 1 fi show_success "Compiled menu bar app" # Download app icons log "Downloading icons..." spin_process "Downloading icons..." \ bash -c "gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/assets/AppIcon.icns -H 'Accept: application/vnd.github.raw' > /tmp/AppIcon.icns && gh api repos/$GH_OWNER/$GH_REPO/contents/$GH_PATH_PREFIX/assets/MenuBarIcon.png -H 'Accept: application/vnd.github.raw' > /tmp/MenuBarIcon.png 2>/dev/null || true" show_success "Downloaded icons" # Create .app bundle structure log "Creating app bundle..." BUNDLE_DIR="$APP_DIR/$APP_NAME.app" BUNDLE_CONTENTS="$BUNDLE_DIR/Contents" BUNDLE_MACOS="$BUNDLE_CONTENTS/MacOS" BUNDLE_RESOURCES="$BUNDLE_CONTENTS/Resources" spin_process "Creating app bundle..." \ bash -c " rm -rf '$BUNDLE_DIR' mkdir -p '$BUNDLE_MACOS' mkdir -p '$BUNDLE_RESOURCES' mv '$APP_DIR/$APP_EXECUTABLE' '$BUNDLE_MACOS/$APP_EXECUTABLE' rm -rf '$APP_DIR/src' mv /tmp/AppIcon.icns '$BUNDLE_RESOURCES/AppIcon.icns' mv /tmp/MenuBarIcon.png '$BUNDLE_RESOURCES/MenuBarIcon.png' 2>/dev/null || true " # Create Info.plist (using variables from config.json) cat > "$BUNDLE_CONTENTS/Info.plist" << PLIST CFBundleExecutable $APP_EXECUTABLE CFBundleIdentifier $BUNDLE_ID CFBundleName $APP_NAME CFBundleDisplayName $APP_NAME CFBundleVersion 6 CFBundleShortVersionString 2.0.0 CFBundlePackageType APPL CFBundleIconFile AppIcon LSMinimumSystemVersion 12.0 LSUIElement NSHighResolutionCapable PLIST show_success "Created app bundle" : ------------------------------------------ : Phase 7: Launch menu bar app : ------------------------------------------ show_step "Launching app" spin_process "Stopping previous instances..." \ bash -c "pkill -f '$APP_EXECUTABLE.app' 2>/dev/null; sleep 0.3; exit 0" show_success "Stopped previous instances" # Launch the app spin_process "Launching menu bar app..." \ bash -c "open '$BUNDLE_DIR' && sleep 0.5" show_success "Launched menu bar app" # Wait for initial sync spin_process "Running initial sync..." sleep 3 show_success "Initial sync complete" : ========================================== : Setup Complete : ========================================== log "Setup completed successfully" show_completion show_info "Log file: $LOG_FILE"