#!/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"