- Add ./setup script for single-command device initialization - Interactive menu: new setup, join existing, or pair mobile - Bootstrap from git repo + age key: ./setup <url> <key> - Mobile pairing with live camera QR scanning - Enhance usync with device management commands - usync init: bootstrap from config repo - usync invite: show QR for mobile - usync add-device: add device by ID - usync join: join existing setup - Mobile/GrapheneOS optimizations - Live camera preview with targeting box (ffmpeg + SDL) - Auto-detect QR codes without manual capture - Syncthing pending device detection - Simplify nix develop shell message - Add ffmpeg, zbar, qrencode, age, sops to devShell Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
513 lines
14 KiB
Bash
Executable File
513 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# usync - Unified sync & setup for Ultimate Notetaking System
|
|
#
|
|
# SETUP (one-liner):
|
|
# usync init <git-url> <age-key>
|
|
# 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 <git-url> <age-key>"
|
|
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 '(?<=<device id=")[^"]+' "$config_file" | head -1 || true)
|
|
if [[ -n "$st_id" ]]; then
|
|
echo "$st_id"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
# Generate new syncthing keys if needed
|
|
if command -v syncthing &>/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 <phone-device-id>${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 <device-id> [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 <git-url> <age-key> Bootstrap from config repo
|
|
usync join Join existing setup (shows QR)
|
|
|
|
PAIRING:
|
|
usync invite Show QR code for mobile to scan
|
|
usync add-device <id> [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
|