#!/usr/bin/env bash # # usync - Unified sync & setup for Ultimate Notetaking System # # SETUP (one-liner): # usync init # usync init git@server:user/config.git AGE-SECRET-KEY-1XXXXX... # # MOBILE PAIRING: # usync invite # Show QR code for phone to scan # usync join # Join from this device using invite # # SYNC: # usync # Sync everything # usync nb # Sync only nb notebooks # usync st # Sync only Syncthing folders # usync status # Show sync status set -euo pipefail USYNC_DIR="${USYNC_DIR:-$HOME/.config/usync}" USYNC_DEVICES="$USYNC_DIR/devices.json" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' log_info() { echo -e "${BLUE}::${NC} $1"; } log_success() { echo -e "${GREEN}OK${NC} $1"; } log_warn() { echo -e "${YELLOW}!!${NC} $1"; } log_error() { echo -e "${RED}ERR${NC} $1" >&2; } log_step() { echo -e "${DIM}->>${NC} $1"; } die() { log_error "$1"; exit 1; } # Check dependencies need_cmd() { command -v "$1" &>/dev/null || die "Required: $1 (not found)" } ensure_dir() { mkdir -p "$USYNC_DIR" } # ============================================================================ # INIT - Bootstrap new device from git repo + age key # ============================================================================ cmd_init() { local git_url="${1:-}" local age_key="${2:-}" if [[ -z "$git_url" ]]; then echo "Usage: usync init " echo "" echo "Examples:" echo " usync init git@server:user/config.git AGE-SECRET-KEY-1XXXX..." echo " usync init https://git.example.com/user/config.git @/path/to/key.txt" echo "" echo "The age key can be:" echo " - The key directly: AGE-SECRET-KEY-1..." echo " - A file path with @: @/path/to/age-key.txt" exit 1 fi if [[ -z "$age_key" ]]; then echo -n "Age secret key (or @/path/to/key.txt): " read -r age_key fi need_cmd git need_cmd age need_cmd sops ensure_dir log_info "Initializing device from $git_url" # Handle @file syntax for age key if [[ "$age_key" == @* ]]; then local keyfile="${age_key:1}" [[ -f "$keyfile" ]] || die "Key file not found: $keyfile" age_key=$(cat "$keyfile") fi # Validate age key format if [[ ! "$age_key" =~ ^AGE-SECRET-KEY-1[A-Z0-9]+$ ]]; then die "Invalid age key format. Expected: AGE-SECRET-KEY-1..." fi # Store age key securely local age_key_file="$USYNC_DIR/age-key.txt" echo "$age_key" > "$age_key_file" chmod 600 "$age_key_file" log_step "Stored age key" # Clone config repo local config_dir="$USYNC_DIR/config" if [[ -d "$config_dir" ]]; then log_step "Updating existing config..." git -C "$config_dir" pull --ff-only else log_step "Cloning config repo..." git clone "$git_url" "$config_dir" fi # Set up sops to use our age key export SOPS_AGE_KEY="$age_key" # Decrypt secrets if they exist if [[ -f "$config_dir/secrets/secrets.yaml" ]]; then log_step "Decrypting secrets..." sops -d "$config_dir/secrets/secrets.yaml" > "$USYNC_DIR/secrets.yaml" chmod 600 "$USYNC_DIR/secrets.yaml" fi # Get/generate device ID local device_id device_id=$(get_or_create_device_id) # Register this device register_device "$device_id" log_success "Device initialized!" echo "" echo "Your device ID:" echo -e "${BOLD}$device_id${NC}" echo "" echo "Next steps:" echo " 1. Add this device ID to other machines" echo " 2. Run: usync invite # to pair mobile devices" echo " 3. Run: usync # to sync" } # ============================================================================ # DEVICE ID MANAGEMENT # ============================================================================ get_or_create_device_id() { # Try syncthing first if command -v syncthing &>/dev/null; then local st_id st_id=$(syncthing cli show system 2>/dev/null | jq -r '.myID // empty' 2>/dev/null || true) if [[ -n "$st_id" ]]; then echo "$st_id" return fi fi # Try to get from config local config_file="$HOME/.config/syncthing/config.xml" if [[ -f "$config_file" ]]; then local st_id st_id=$(grep -oP '(?<=/dev/null; then log_step "Generating Syncthing device ID..." syncthing generate --skip-port-probing &>/dev/null || true syncthing cli show system 2>/dev/null | jq -r '.myID' 2>/dev/null || echo "UNKNOWN" else echo "UNKNOWN-$(hostname)-$(date +%s)" fi } register_device() { local device_id="$1" local hostname hostname=$(hostname) ensure_dir # Load or create devices file if [[ ! -f "$USYNC_DEVICES" ]]; then echo '{"devices":{}}' > "$USYNC_DEVICES" fi # Add this device local tmp tmp=$(mktemp) jq --arg id "$device_id" --arg name "$hostname" \ '.devices[$name] = {id: $id, added: (now | todate)}' \ "$USYNC_DEVICES" > "$tmp" && mv "$tmp" "$USYNC_DEVICES" log_step "Registered device: $hostname" } # ============================================================================ # INVITE - Generate QR/invite for mobile device # ============================================================================ cmd_invite() { local device_id device_id=$(get_or_create_device_id) echo "" echo -e "${BOLD}=== Add Mobile Device ===${NC}" echo "" echo "Device ID:" echo -e "${GREEN}$device_id${NC}" echo "" # Generate QR code if qrencode is available if command -v qrencode &>/dev/null; then echo "Scan this QR code with Syncthing app:" echo "" qrencode -t ANSIUTF8 "$device_id" echo "" else log_warn "Install 'qrencode' for QR codes: nix-shell -p qrencode" echo "" fi echo -e "${DIM}--- Manual setup ---${NC}" echo "" echo "In Syncthing Android app:" echo " 1. Open Syncthing" echo " 2. Tap menu (3 dots) > Show device ID" echo " 3. Run on this machine:" echo "" echo -e " ${BOLD}usync add-device ${NC}" echo "" echo " 4. Back in app: Add Device > paste this ID:" echo "" echo -e " ${GREEN}$device_id${NC}" echo "" # Show folders that can be shared if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then local folders folders=$(syncthing cli show config 2>/dev/null | jq -r '.folders[].id' 2>/dev/null | tr '\n' ' ') if [[ -n "$folders" ]]; then echo "Available folders to share: $folders" fi fi } # ============================================================================ # ADD-DEVICE - Add a device by ID (for mobile pairing) # ============================================================================ cmd_add_device() { local device_id="${1:-}" local device_name="${2:-mobile}" if [[ -z "$device_id" ]]; then echo "Usage: usync add-device [name]" echo "" echo "Get the device ID from Syncthing app:" echo " Menu > Show device ID" exit 1 fi # Clean up device ID (remove spaces, lowercase) device_id=$(echo "$device_id" | tr -d ' \n' | tr '[:lower:]' '[:upper:]') # Validate format (basic check) if [[ ! "$device_id" =~ ^[A-Z0-9-]{52,63}$ ]]; then log_warn "Device ID looks unusual (expected 52-63 chars). Continuing anyway..." fi if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then log_info "Adding device via Syncthing CLI..." # Add device to syncthing syncthing cli config devices add --device-id "$device_id" --name "$device_name" # Auto-accept folders from this device (convenient for mobile) syncthing cli config devices "$device_id" auto-accept-folders set true log_success "Added device: $device_name ($device_id)" echo "" echo "The device should appear in Syncthing shortly." echo "You may need to accept the connection on the mobile device." else # Store for later if syncthing not running register_device_manual "$device_id" "$device_name" log_success "Stored device: $device_name" echo "Device will be added when Syncthing starts." fi } register_device_manual() { local device_id="$1" local device_name="$2" ensure_dir if [[ ! -f "$USYNC_DEVICES" ]]; then echo '{"devices":{}}' > "$USYNC_DEVICES" fi local tmp tmp=$(mktemp) jq --arg id "$device_id" --arg name "$device_name" \ '.devices[$name] = {id: $id, added: (now | todate), pending: true}' \ "$USYNC_DEVICES" > "$tmp" && mv "$tmp" "$USYNC_DEVICES" } # ============================================================================ # JOIN - Join an existing setup (interactive, mobile-friendly output) # ============================================================================ cmd_join() { echo "" echo -e "${BOLD}=== Join Existing Setup ===${NC}" echo "" local device_id device_id=$(get_or_create_device_id) echo "This device's ID:" echo "" echo -e "${GREEN}$device_id${NC}" echo "" # Generate QR for easy sharing if command -v qrencode &>/dev/null; then echo "Other devices can scan this:" qrencode -t ANSIUTF8 "$device_id" echo "" fi echo "To complete setup:" echo " 1. On another device, run:" echo -e " ${BOLD}usync add-device $device_id $(hostname)${NC}" echo "" echo " 2. Or add manually in Syncthing GUI" echo "" echo -n "Press Enter when the other device has added you..." read -r # Trigger a rescan if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then syncthing cli scan --all 2>/dev/null || true fi log_success "Done! Folders will sync automatically." } # ============================================================================ # SYNC COMMANDS (original functionality) # ============================================================================ sync_nb() { log_info "Syncing nb notebooks..." if ! command -v nb &>/dev/null; then log_warn "nb not found" return 0 fi local notebooks notebooks=$(nb notebooks --names 2>/dev/null || echo "") if [[ -z "$notebooks" ]]; then log_warn "No nb notebooks found" return 0 fi for notebook in $notebooks; do log_step "Syncing: $notebook" if nb "$notebook:sync" 2>/dev/null; then log_success "$notebook" else log_warn "$notebook (no remote?)" fi done } sync_syncthing() { log_info "Triggering Syncthing scan..." if ! command -v syncthing &>/dev/null; then log_warn "syncthing not found" return 0 fi if ! syncthing cli show system &>/dev/null 2>&1; then log_warn "Syncthing not running" return 0 fi if syncthing cli scan --all 2>/dev/null; then log_success "Scan triggered" else log_warn "Scan failed" fi } show_status() { echo -e "${BOLD}=== Sync Status ===${NC}" echo "" # Device ID local device_id device_id=$(get_or_create_device_id 2>/dev/null || echo "unknown") echo -e "This device: ${DIM}${device_id:0:20}...${NC}" echo "" # nb echo "nb notebooks:" if command -v nb &>/dev/null; then local nb_dir="${NB_DIR:-$HOME/.nb}" for notebook in $(nb notebooks --names 2>/dev/null || true); do if [[ -d "$nb_dir/$notebook/.git" ]]; then local last last=$(git -C "$nb_dir/$notebook" log -1 --format="%ar" 2>/dev/null || echo "never") echo -e " ${GREEN}*${NC} $notebook (synced $last)" else echo -e " ${YELLOW}*${NC} $notebook (local only)" fi done else echo " (nb not installed)" fi echo "" # Syncthing echo "Syncthing:" if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then local folders folders=$(syncthing cli show config 2>/dev/null | jq -r '.folders[] | " \(.id): \(.path)"' 2>/dev/null || echo " (error)") echo "$folders" echo "" echo "Connected devices:" syncthing cli show connections 2>/dev/null | jq -r '.connections | to_entries[] | select(.value.connected) | " \(.key[:15])... (\(.value.type))"' 2>/dev/null || echo " (none)" else echo " (not running)" fi } # ============================================================================ # MAIN # ============================================================================ show_help() { cat <<'EOF' usync - Unified sync & setup SETUP (first time): usync init Bootstrap from config repo usync join Join existing setup (shows QR) PAIRING: usync invite Show QR code for mobile to scan usync add-device [name] Add a device by ID SYNC: usync Sync everything usync nb Sync nb notebooks only usync st Sync Syncthing only usync status Show sync status EXAMPLES: # Bootstrap new desktop from config repo: usync init git@server:me/config.git AGE-SECRET-KEY-1XXXX... # Pair mobile phone: usync invite # on desktop, scan QR with phone usync add-device XXXXX mobile # after getting phone's ID # Daily sync: usync EOF } case "${1:-sync}" in init) shift cmd_init "$@" ;; invite|pair|qr) cmd_invite ;; add-device|add) shift cmd_add_device "$@" ;; join) cmd_join ;; nb) sync_nb ;; st|syncthing) sync_syncthing ;; status|s) show_status ;; sync|"") sync_nb echo "" sync_syncthing ;; -h|--help|help) show_help ;; *) log_error "Unknown command: $1" echo "Run 'usync help' for usage" exit 1 ;; esac