#!/usr/bin/env bash # # setup - One command setup for Ultimate Notetaking System # # Usage: # ./setup # Interactive setup # ./setup # Non-interactive join # ./setup mobile # Pair mobile device # set -euo pipefail # Colors R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' B='\033[0;34m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' info() { echo -e "${B}::${NC} $1"; } ok() { echo -e "${G}OK${NC} $1"; } warn() { echo -e "${Y}!!${NC} $1"; } err() { echo -e "${R}ERR${NC} $1" >&2; } step() { echo -e "${DIM}->>${NC} $1"; } # Config USYNC_DIR="$HOME/.config/usync" AGE_KEY_FILE="$USYNC_DIR/age-key.txt" has() { command -v "$1" &>/dev/null; } ensure_deps() { local missing=() for cmd in git jq; do has "$cmd" || missing+=("$cmd") done if [[ ${#missing[@]} -gt 0 ]]; then err "Missing: ${missing[*]}" echo "Install with: nix-shell -p ${missing[*]}" exit 1 fi # Optional but recommended has syncthing || warn "syncthing not found - file sync won't work" has age || warn "age not found - install for secret management" has qrencode || warn "qrencode not found - QR codes won't work" } get_device_id() { if has syncthing; then # Try running instance local id id=$(syncthing cli show system 2>/dev/null | jq -r '.myID // empty' 2>/dev/null || true) [[ -n "$id" ]] && { echo "$id"; return; } # Try config file local cfg="$HOME/.config/syncthing/config.xml" if [[ -f "$cfg" ]]; then id=$(grep -oP '(?<=/dev/null | head -1 || true) [[ -n "$id" ]] && { echo "$id"; return; } fi # Generate new step "Generating Syncthing identity..." syncthing generate --skip-port-probing &>/dev/null || true syncthing cli show system 2>/dev/null | jq -r '.myID' 2>/dev/null || echo "" fi } # ============================================================================= # SETUP NEW (first device) # ============================================================================= setup_new() { echo "" echo -e "${BOLD}Setting up first device...${NC}" echo "" mkdir -p "$USYNC_DIR" # Generate age key if [[ -f "$AGE_KEY_FILE" ]]; then warn "Age key already exists at $AGE_KEY_FILE" else if has age-keygen; then step "Generating age encryption key..." age-keygen -o "$AGE_KEY_FILE" 2>&1 | grep "public key" chmod 600 "$AGE_KEY_FILE" else err "age not installed. Run: nix-shell -p age" exit 1 fi fi # Get/create syncthing ID local device_id device_id=$(get_device_id) # Create local config structure local config_dir="$USYNC_DIR/config" if [[ ! -d "$config_dir" ]]; then step "Creating config structure..." mkdir -p "$config_dir/hosts/$(hostname)" "$config_dir/secrets" # Create minimal secrets template cat > "$config_dir/secrets/secrets.yaml" <<'EOF' # Encrypted with sops - edit with: sops secrets/secrets.yaml restic-password: "changeme" EOF # Init git repo git -C "$config_dir" init -q git -C "$config_dir" add -A git -C "$config_dir" commit -q -m "Initial setup" fi # Summary echo "" echo -e "${G}=== Setup Complete ===${NC}" echo "" echo "Your age key (SAVE THIS SOMEWHERE SAFE):" echo -e "${BOLD}$(cat "$AGE_KEY_FILE" | grep 'AGE-SECRET-KEY')${NC}" echo "" if [[ -n "$device_id" ]]; then echo "Device ID: ${device_id:0:20}..." fi echo "" echo "Next steps:" echo " 1. Push config to git server:" echo " cd $config_dir && git remote add origin && git push -u origin main" echo "" echo " 2. On other devices, run:" echo " ./setup " echo "" echo " 3. To pair mobile:" echo " ./setup mobile" } # ============================================================================= # SETUP JOIN (existing setup) # ============================================================================= setup_join() { local git_url="$1" local age_key="$2" echo "" echo -e "${BOLD}Joining existing setup...${NC}" echo "" mkdir -p "$USYNC_DIR" # Handle @file syntax if [[ "$age_key" == @* ]]; then local keyfile="${age_key:1}" [[ -f "$keyfile" ]] || { err "Key file not found: $keyfile"; exit 1; } age_key=$(cat "$keyfile") fi # Save age key echo "$age_key" > "$AGE_KEY_FILE" chmod 600 "$AGE_KEY_FILE" step "Saved age key" # Clone config local config_dir="$USYNC_DIR/config" if [[ -d "$config_dir" ]]; then step "Updating config..." git -C "$config_dir" pull --ff-only 2>/dev/null || git -C "$config_dir" fetch else step "Cloning config..." git clone "$git_url" "$config_dir" fi # Decrypt secrets if has sops && [[ -f "$config_dir/secrets/secrets.yaml" ]]; then step "Decrypting secrets..." export SOPS_AGE_KEY="$age_key" sops -d "$config_dir/secrets/secrets.yaml" > "$USYNC_DIR/secrets.yaml" 2>/dev/null || warn "Could not decrypt secrets" chmod 600 "$USYNC_DIR/secrets.yaml" 2>/dev/null || true fi # Setup syncthing local device_id device_id=$(get_device_id) # Register device local devices_file="$USYNC_DIR/devices.json" echo "{\"devices\":{\"$(hostname)\":{\"id\":\"$device_id\",\"added\":\"$(date -Iseconds)\"}}}" > "$devices_file" echo "" echo -e "${G}=== Joined! ===${NC}" echo "" if [[ -n "$device_id" ]]; then echo "This device: ${device_id:0:20}..." echo "" echo "Add this device on your other machines:" echo -e " ${BOLD}./setup add $device_id $(hostname)${NC}" fi echo "" echo "To pair mobile: ./setup mobile" echo "To sync: ./scripts/usync" } # ============================================================================= # MOBILE PAIRING # ============================================================================= scan_qr_camera() { # Check for video devices if [[ ! -e /dev/video0 ]]; then err "No camera found (/dev/video0 doesn't exist)" >&2 return 1 fi if ! has ffmpeg; then err "ffmpeg not found. Run: nix-shell -p ffmpeg zbar" >&2 return 1 fi if ! has zbarimg; then err "zbarimg not found. Run: nix-shell -p zbar" >&2 return 1 fi local tmpdir="/tmp/usync_qr_$$" local result_file="$tmpdir/result" local ffmpeg_pid="" mkdir -p "$tmpdir" cleanup_camera() { [[ -n "$ffmpeg_pid" ]] && kill "$ffmpeg_pid" 2>/dev/null || true rm -rf "$tmpdir" } trap cleanup_camera RETURN EXIT echo "" >&2 echo "Point camera at QR code - it will scan automatically." >&2 echo "(Press Ctrl+C to cancel)" >&2 echo "" >&2 # Start ffmpeg: show preview with crosshair AND save frames for scanning ffmpeg -f v4l2 -video_size 640x480 -i /dev/video0 \ -vf "drawbox=x=iw/2-100:y=ih/2-100:w=200:h=200:color=green:t=3" \ -f sdl2 -window_title "Scan QR Code" - \ -vf "fps=2" -update 1 -q:v 2 "$tmpdir/frame.jpg" \ -loglevel quiet 2>/dev/null & ffmpeg_pid=$! sleep 1 # Let camera warm up # Scan frames as they're captured for attempt in {1..60}; do sleep 0.5 # Check if ffmpeg died if ! kill -0 "$ffmpeg_pid" 2>/dev/null; then break fi if [[ -f "$tmpdir/frame.jpg" ]]; then local result result=$(zbarimg --raw -q "$tmpdir/frame.jpg" 2>/dev/null || true) if [[ -n "$result" ]]; then result=$(echo "$result" | tr -d ' \n\r' | tr '[:lower:]' '[:upper:]') if [[ "$result" =~ ^[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}$ ]]; then echo "" >&2 ok "Scanned!" >&2 echo "$result" return 0 fi fi fi done echo "" >&2 warn "No QR code detected (timed out after 30s)" >&2 return 1 } check_pending_devices() { if ! has syncthing || ! syncthing cli show system &>/dev/null 2>&1; then return 1 fi # Get pending devices (devices trying to connect that we haven't approved) local pending pending=$(syncthing cli show pending devices 2>/dev/null || true) if [[ -n "$pending" && "$pending" != "{}" && "$pending" != "null" ]]; then echo "$pending" | jq -r 'to_entries[] | "\(.key)|\(.value.name // "unknown")"' 2>/dev/null return 0 fi return 1 } check_local_discovery() { if ! has syncthing || ! syncthing cli show system &>/dev/null 2>&1; then return 1 fi # Check discovery cache for LAN devices local discovery discovery=$(syncthing cli show discovery 2>/dev/null || true) if [[ -n "$discovery" && "$discovery" != "{}" && "$discovery" != "null" ]]; then echo "$discovery" | jq -r 'to_entries[] | select(.value[] | contains("tcp://192.") or contains("tcp://10.") or contains("tcp://172.")) | .key' 2>/dev/null | head -5 return 0 fi return 1 } add_phone_device() { local phone_id="$1" local name="${2:-phone}" phone_id=$(echo "$phone_id" | tr -d ' \n' | tr '[:lower:]' '[:upper:]') if has syncthing && syncthing cli show system &>/dev/null 2>&1; then syncthing cli config devices add --device-id "$phone_id" --name "$name" 2>/dev/null || true syncthing cli config devices "$phone_id" auto-accept-folders set true 2>/dev/null || true ok "Added: $name" return 0 else mkdir -p "$USYNC_DIR" echo "$phone_id" > "$USYNC_DIR/pending-phone.id" ok "Saved (will add when syncthing starts)" return 0 fi } setup_mobile() { echo "" echo -e "${BOLD}=== Pair Mobile Device ===${NC}" echo "" local device_id device_id=$(get_device_id) if [[ -z "$device_id" ]]; then err "Could not get device ID. Is syncthing installed?" exit 1 fi # Check for pending devices first (phone already trying to connect) local pending pending=$(check_pending_devices 2>/dev/null || true) if [[ -n "$pending" ]]; then echo -e "${G}Found device(s) waiting to connect:${NC}" echo "" local i=1 declare -a pending_ids while IFS='|' read -r id name; do echo " [$i] $name (${id:0:20}...)" pending_ids+=("$id") ((i++)) done <<< "$pending" echo "" echo -n "Accept which? [1-$((i-1))], or Enter to continue: " read -r choice if [[ -n "$choice" && "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -lt "$i" ]]; then local selected_id="${pending_ids[$((choice-1))]}" add_phone_device "$selected_id" "phone" echo "" ok "Mobile pairing complete!" return fi echo "" fi # Step 1: Show computer's ID for phone echo -e "${BOLD}Step 1: Add this computer on your phone${NC}" echo "" echo "Computer's Device ID:" echo "" echo -e "${G}$device_id${NC}" echo "" if has qrencode; then echo "Or scan this QR in Syncthing app:" echo "" qrencode -t ANSIUTF8 "$device_id" fi echo "" echo "In Syncthing app: tap (+) > paste or scan above" echo "" echo -n "Press Enter when done..." read -r # Step 2: Get phone's ID echo "" echo -e "${BOLD}Step 2: Add your phone on this computer${NC}" echo "" echo "In Syncthing app: Menu (3 dots) > Show device ID" echo "" # Try camera first if available if has zbarimg && has ffmpeg && [[ -e /dev/video0 ]]; then echo " [1] Scan phone's QR with camera" echo " [2] Paste phone's device ID" echo "" echo -n "Choice [1/2]: " read -r method if [[ "$method" == "1" ]]; then local scanned if scanned=$(scan_qr_camera); then add_phone_device "$scanned" "phone" echo "" ok "Pairing complete! Devices should connect shortly." return fi echo "" echo "Camera didn't work. Paste the ID instead:" fi fi echo -n "Paste phone's device ID: " read -r phone_id if [[ -n "$phone_id" ]]; then add_phone_device "$phone_id" "phone" echo "" ok "Pairing complete! Devices should connect shortly." else warn "No ID entered. Run './setup mobile' to try again." fi } # ============================================================================= # ADD DEVICE # ============================================================================= add_device() { local device_id="$1" local name="${2:-device}" device_id=$(echo "$device_id" | tr -d ' \n' | tr '[:lower:]' '[:upper:]') if has syncthing && syncthing cli show system &>/dev/null 2>&1; then syncthing cli config devices add --device-id "$device_id" --name "$name" syncthing cli config devices "$device_id" auto-accept-folders set true ok "Added device: $name" else mkdir -p "$USYNC_DIR" echo "{\"id\":\"$device_id\",\"name\":\"$name\"}" >> "$USYNC_DIR/pending-devices.json" ok "Saved device (will add when syncthing runs)" fi } # ============================================================================= # INTERACTIVE # ============================================================================= interactive() { echo "" echo -e "${BOLD}Ultimate Notetaking System - Setup${NC}" echo "" echo " [1] New setup (first device)" echo " [2] Join existing setup" echo " [3] Pair mobile device" echo "" echo -n "Choice [1/2/3]: " read -r choice case "$choice" in 1|n|new) setup_new ;; 2|j|join) echo "" echo -n "Config git URL: " read -r git_url echo -n "Age secret key: " read -r age_key setup_join "$git_url" "$age_key" ;; 3|m|mobile) setup_mobile ;; *) err "Invalid choice" exit 1 ;; esac } # ============================================================================= # MAIN # ============================================================================= ensure_deps case "${1:-}" in "") interactive ;; mobile|phone|m) setup_mobile ;; add) [[ -z "${2:-}" ]] && { err "Usage: ./setup add [name]"; exit 1; } add_device "$2" "${3:-}" ;; help|-h|--help) cat <<'EOF' setup - One command setup Usage: ./setup Interactive setup ./setup Join existing setup ./setup mobile Pair mobile device ./setup add [name] Add a device Examples: ./setup # Interactive ./setup git@srv:me/cfg.git AGE-SECRET-KEY-1... # Join ./setup mobile # Pair phone EOF ;; *) # Assume it's: ./setup if [[ -n "${2:-}" ]]; then setup_join "$1" "$2" else err "Usage: ./setup " exit 1 fi ;; esac