#!/usr/bin/env bash # # Metool Bootstrap Script # # This script sets up a fresh Mac or Linux workstation with: # 1. Metool - modular shell environment management # 2. Metool-packages - public package collection (including keycutter) # 3. Keycutter - SSH key management with FIDO/YubiKey support # 4. (Optional) Private packages module after SSH setup # # Quick install: # curl -fsSL https://raw.githubusercontent.com/mbailey/metool/main/contrib/bootstrap | bash # # Or with options: # curl -fsSL https://raw.githubusercontent.com/mbailey/metool/main/contrib/bootstrap | bash -s -- --skip-keycutter # set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color # Logging functions log() { echo -e "${GREEN}[bootstrap]${NC} $*"; } error() { echo -e "${RED}[bootstrap]${NC} $*" >&2; } warn() { echo -e "${YELLOW}[bootstrap]${NC} $*"; } info() { echo -e "${BLUE}[bootstrap]${NC} $*"; } header() { echo -e "\n${BOLD}${CYAN}=== $* ===${NC}\n"; } # Default configuration SKIP_KEYCUTTER=false SKIP_PRIVATE=false PRIVATE_MODULE_URL="" METOOL_INSTALL_URL="https://raw.githubusercontent.com/mbailey/metool/main/install.sh" # Parse arguments parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --skip-keycutter) SKIP_KEYCUTTER=true shift ;; --skip-private) SKIP_PRIVATE=true shift ;; --private-module) PRIVATE_MODULE_URL="$2" shift 2 ;; -h|--help) show_help exit 0 ;; *) error "Unknown option: $1" show_help exit 1 ;; esac done } show_help() { cat << 'EOF' Metool Bootstrap Script Usage: bootstrap [OPTIONS] Options: --skip-keycutter Skip keycutter installation and SSH key setup --skip-private Skip prompting for private module --private-module URL Pre-specify private module URL -h, --help Show this help Examples: # Full bootstrap (interactive) curl -fsSL https://raw.githubusercontent.com/mbailey/metool/main/contrib/bootstrap | bash # Skip keycutter setup curl -fsSL https://raw.githubusercontent.com/mbailey/metool/main/contrib/bootstrap | bash -s -- --skip-keycutter # With pre-specified private module curl -fsSL https://raw.githubusercontent.com/mbailey/metool/main/contrib/bootstrap | bash -s -- --private-module git@github.com:user/metool-packages-dev.git EOF } # Check if command exists command_exists() { command -v "$1" >/dev/null 2>&1 } # Detect OS detect_os() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then echo "linux" elif [[ "$OSTYPE" == "darwin"* ]]; then echo "macos" else echo "unsupported" fi } # Check prerequisites check_prerequisites() { header "Checking Prerequisites" local os=$(detect_os) local missing_deps=() # Git is required if ! command_exists git; then missing_deps+=("git") else log "Git installed" fi # Curl is required for downloading if ! command_exists curl; then missing_deps+=("curl") else log "Curl installed" fi # macOS-specific checks if [[ "$os" == "macos" ]]; then if ! command_exists brew; then warn "Homebrew not installed" info "Installing Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Add Homebrew to PATH for this session if [[ -f /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -f /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" fi fi log "Homebrew installed" fi # Install missing dependencies if [[ ${#missing_deps[@]} -gt 0 ]]; then error "Missing dependencies: ${missing_deps[*]}" if [[ "$os" == "macos" ]]; then info "Installing with Homebrew..." brew install "${missing_deps[@]}" elif [[ "$os" == "linux" ]]; then if command_exists apt-get; then info "Installing with apt..." sudo apt-get update && sudo apt-get install -y "${missing_deps[@]}" elif command_exists dnf; then info "Installing with dnf..." sudo dnf install -y "${missing_deps[@]}" else error "Please install manually: ${missing_deps[*]}" exit 1 fi fi fi log "All prerequisites met" } # Install metool install_metool() { header "Installing Metool" # Check if metool is already installed if command_exists mt || command_exists mtbin; then log "Metool appears to be installed" # Source metool to ensure it's available if [[ -f "$HOME/.metool/shell/metool/mt" ]]; then source "$HOME/.metool/shell/metool/mt" log "Metool loaded" return 0 fi fi # Run metool installer info "Running metool installer..." curl -fsSL "$METOOL_INSTALL_URL" | bash # Source metool for this session if [[ -f "$HOME/.metool/shell/metool/mt" ]]; then source "$HOME/.metool/shell/metool/mt" log "Metool installed and loaded" else error "Metool installation may have failed" error "Please check ~/.metool/ and try again" exit 1 fi } # Add metool-packages module and pull dependencies setup_public_packages() { header "Setting Up Public Packages" # Check if module already added if [[ -L "$HOME/.metool/shell/modules/metool-packages" ]]; then log "metool-packages module already in working set" else info "Adding metool-packages module..." mt module add mbailey/metool-packages fi # Get the module path local module_path module_path=$(readlink -f "$HOME/.metool/shell/modules/metool-packages" 2>/dev/null || echo "") if [[ -z "$module_path" ]] || [[ ! -d "$module_path" ]]; then error "Failed to locate metool-packages module" exit 1 fi # Check for .repos.txt and pull dependencies if [[ -f "$module_path/.repos.txt" ]]; then info "Pulling package dependencies (this may take a few minutes)..." # Run mt git pull in the module directory pushd "$module_path" > /dev/null mt git pull || { warn "Some repositories may have failed to clone" warn "You can retry later with: cd $module_path && mt git pull" } popd > /dev/null log "Package dependencies pulled" else warn "No .repos.txt found in metool-packages" fi } # Install keycutter package install_keycutter() { header "Installing Keycutter" if [[ "$SKIP_KEYCUTTER" == "true" ]]; then info "Skipping keycutter installation (--skip-keycutter)" return 0 fi # Check if keycutter is already available if command_exists keycutter; then log "Keycutter already installed" else # Check if keycutter package exists in metool-packages local keycutter_pkg="$HOME/.metool/shell/modules/metool-packages/keycutter" if [[ -L "$keycutter_pkg" ]] || [[ -d "$keycutter_pkg" ]]; then info "Installing keycutter package..." mt package add metool-packages/keycutter || { warn "Failed to add keycutter to package working set" } mt package install keycutter || { warn "Failed to install keycutter package" warn "You can install manually with: mt package install keycutter" } else warn "Keycutter package not found in metool-packages" warn "It may not have been cloned yet" warn "Try running: cd ~/.metool/shell/modules/metool-packages && mt git pull" fi fi # Guide through keycutter setup if available if command_exists keycutter; then setup_keycutter fi } # Guide user through keycutter setup setup_keycutter() { header "SSH Key Setup with Keycutter" echo info "Keycutter is installed and ready to set up SSH keys." info "FIDO SSH keys provide hardware-backed security for your Git operations." echo # Check if user already has SSH keys local existing_keys=0 if [[ -d "$HOME/.ssh/keycutter/keys" ]]; then existing_keys=$(find "$HOME/.ssh/keycutter/keys" -name "*.pub" 2>/dev/null | wc -l) fi if [[ $existing_keys -gt 0 ]]; then log "Found $existing_keys existing keycutter SSH key(s)" info "To create additional keys: keycutter create github.com_username" return 0 fi echo "Would you like to set up SSH keys now?" echo " 1) Yes, set up SSH keys (recommended)" echo " 2) No, I'll do it later" echo read -p "Choice [1/2]: " -n 1 -r choice echo case "$choice" in 1|"") echo info "To create a GitHub SSH key, you'll need:" info " - A YubiKey or other FIDO2 device" info " - Your GitHub username" echo read -p "Enter your GitHub username: " github_user if [[ -n "$github_user" ]]; then info "Creating SSH key for github.com_${github_user}..." keycutter create "github.com_${github_user}" || { warn "Key creation may have failed" warn "You can try again with: keycutter create github.com_${github_user}" } else warn "No username provided, skipping key creation" info "Create a key later with: keycutter create github.com_username" fi ;; 2) info "SSH key setup skipped" info "Create keys later with: keycutter create github.com_username" ;; *) warn "Invalid choice, skipping SSH key setup" ;; esac } # Set up private packages module setup_private_packages() { header "Private Packages (Optional)" if [[ "$SKIP_PRIVATE" == "true" ]]; then info "Skipping private module setup (--skip-private)" return 0 fi # Use pre-specified URL if provided local private_url="$PRIVATE_MODULE_URL" if [[ -z "$private_url" ]]; then echo info "You can add a private packages module (requires SSH keys)." info "This is typically for personal/work-specific packages." echo echo "Enter private module URL (or press Enter to skip):" echo " Example: git@github.com:username/metool-packages-dev.git" echo " Example: username/metool-packages-dev" echo read -p "Private module URL: " private_url fi if [[ -z "$private_url" ]]; then info "No private module specified, skipping" return 0 fi info "Adding private module: $private_url" if mt module add "$private_url"; then log "Private module added" # Extract module name for pulling local module_name module_name=$(basename "${private_url%.git}") local module_path="$HOME/.metool/shell/modules/$module_name" if [[ -d "$module_path" ]] && [[ -f "$module_path/.repos.txt" ]]; then info "Pulling private module dependencies..." pushd "$module_path" > /dev/null mt git pull || warn "Some dependencies may have failed" popd > /dev/null fi echo info "Private module ready!" info "List packages with: mt package list $module_name/*" info "Install packages with: mt package add $module_name/package-name && mt package install package-name" else error "Failed to add private module" error "This may be because SSH keys aren't set up yet" info "After setting up SSH, add module with: mt module add $private_url" fi } # Print summary print_summary() { header "Bootstrap Complete!" echo -e "${GREEN}Metool is now installed and configured.${NC}" echo echo "Quick reference:" echo " mt module list # List installed modules" echo " mt package list # List available packages" echo " mt package install PKG # Install a package" echo " mt --help # Show all commands" echo if command_exists keycutter; then echo "Keycutter commands:" echo " keycutter create SERVICE_USER # Create SSH key" echo " keycutter --help # Show all commands" echo fi echo "Next steps:" echo " 1. Start a new terminal (or run: source ~/.bashrc)" if ! command_exists keycutter || [[ "$SKIP_KEYCUTTER" == "true" ]]; then echo " 2. Set up SSH keys: keycutter create github.com_username" fi echo info "Documentation: https://github.com/mbailey/metool" } # Main function main() { parse_args "$@" echo echo -e "${BOLD}${CYAN}" echo " __ __ _ _ ____ _ _ " echo " | \/ | ___| |_ ___ ___ | | | __ ) ___ ___ | |_ ___| |_ _ __ __ _ _ __ " echo " | |\/| |/ _ \ __/ _ \ / _ \| | | _ \ / _ \ / _ \| __/ __| __| '__/ _\` | '_ \ " echo " | | | | __/ || (_) | (_) | | | |_) | (_) | (_) | |_\__ \ |_| | | (_| | |_) |" echo " |_| |_|\___|\__\___/ \___/|_| |____/ \___/ \___/ \__|___/\__|_| \__,_| .__/ " echo " |_| " echo -e "${NC}" echo check_prerequisites install_metool setup_public_packages install_keycutter setup_private_packages print_summary } # Run main main "$@"