Add one-command setup with mobile QR pairing
- 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>
This commit is contained in:
21
flake.nix
21
flake.nix
@@ -94,6 +94,13 @@
|
||||
restic # Backup
|
||||
rclone # Cloud storage
|
||||
|
||||
# Setup & Pairing
|
||||
qrencode # QR codes for mobile pairing
|
||||
zbar # QR scanning (zbarimg)
|
||||
ffmpeg # Camera preview + capture
|
||||
age # Encryption for secrets
|
||||
sops # Secret management
|
||||
|
||||
# Development
|
||||
git
|
||||
neovim
|
||||
@@ -107,16 +114,12 @@
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Ultimate Notetaking, Sync & Backup System"
|
||||
printf '\033[1m%s\033[0m\n' "Ultimate Notetaking, Sync & Backup System"
|
||||
echo ""
|
||||
echo "Available tools:"
|
||||
echo " nb - Note-taking CLI"
|
||||
echo " syncthing - File synchronization"
|
||||
echo " unison - Alternative file sync"
|
||||
echo " restic - Backup"
|
||||
echo " rclone - Cloud storage"
|
||||
echo ""
|
||||
echo "Run 'nb help' to get started with notes."
|
||||
echo "Get started: ./setup"
|
||||
echo "Pair mobile: ./setup mobile"
|
||||
echo "Sync: ./scripts/usync"
|
||||
echo "Status: ./scripts/ustatus"
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
490
scripts/usync
490
scripts/usync
@@ -1,63 +1,375 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# usync - Unified sync command for the Ultimate Notetaking System
|
||||
# usync - Unified sync & setup for Ultimate Notetaking System
|
||||
#
|
||||
# Syncs both nb notebooks and triggers Syncthing scans.
|
||||
# SETUP (one-liner):
|
||||
# usync init <git-url> <age-key>
|
||||
# usync init git@server:user/config.git AGE-SECRET-KEY-1XXXXX...
|
||||
#
|
||||
# Usage:
|
||||
# usync - Sync everything
|
||||
# usync nb - Sync only nb notebooks
|
||||
# usync st - Sync only Syncthing folders
|
||||
# usync status - Show sync status
|
||||
# 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'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
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)"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[OK]${NC} $1"
|
||||
ensure_dir() {
|
||||
mkdir -p "$USYNC_DIR"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
# ============================================================================
|
||||
# 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"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
# ============================================================================
|
||||
# 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_error "nb not found in PATH"
|
||||
return 1
|
||||
log_warn "nb not found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get list of notebooks
|
||||
local notebooks
|
||||
notebooks=$(nb notebooks --names 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$notebooks" ]; then
|
||||
if [[ -z "$notebooks" ]]; then
|
||||
log_warn "No nb notebooks found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Sync each notebook
|
||||
for notebook in $notebooks; do
|
||||
log_info " Syncing notebook: $notebook"
|
||||
log_step "Syncing: $notebook"
|
||||
if nb "$notebook:sync" 2>/dev/null; then
|
||||
log_success " $notebook synced"
|
||||
log_success "$notebook"
|
||||
else
|
||||
log_warn " $notebook sync failed (no remote?)"
|
||||
log_warn "$notebook (no remote?)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
@@ -66,87 +378,131 @@ sync_syncthing() {
|
||||
log_info "Triggering Syncthing scan..."
|
||||
|
||||
if ! command -v syncthing &>/dev/null; then
|
||||
log_error "syncthing not found in PATH"
|
||||
return 1
|
||||
log_warn "syncthing not found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if Syncthing is running
|
||||
if ! syncthing cli show system &> /dev/null; then
|
||||
log_warn "Syncthing is not running"
|
||||
return 1
|
||||
if ! syncthing cli show system &>/dev/null 2>&1; then
|
||||
log_warn "Syncthing not running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Trigger scan on all folders
|
||||
if syncthing cli scan --all 2>/dev/null; then
|
||||
log_success "Syncthing scan triggered"
|
||||
log_success "Scan triggered"
|
||||
else
|
||||
log_error "Failed to trigger Syncthing scan"
|
||||
return 1
|
||||
log_warn "Scan failed"
|
||||
fi
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo "=== Sync Status ==="
|
||||
echo -e "${BOLD}=== Sync Status ===${NC}"
|
||||
echo ""
|
||||
|
||||
# nb status
|
||||
echo "--- nb Notebooks ---"
|
||||
if command -v nb &> /dev/null; then
|
||||
nb notebooks 2>/dev/null || echo " No notebooks"
|
||||
# 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 ""
|
||||
echo "Last sync times:"
|
||||
for notebook in $(nb notebooks --names 2>/dev/null); do
|
||||
nb_dir="${NB_DIR:-$HOME/.nb}"
|
||||
if [ -d "$nb_dir/$notebook/.git" ]; then
|
||||
last_commit=$(git -C "$nb_dir/$notebook" log -1 --format="%ar" 2>/dev/null || echo "never")
|
||||
echo " $notebook: $last_commit"
|
||||
|
||||
# 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"
|
||||
echo " (nb not installed)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Syncthing status
|
||||
echo "--- Syncthing ---"
|
||||
if command -v syncthing &> /dev/null && syncthing cli show system &> /dev/null; then
|
||||
syncthing cli show system 2>/dev/null | grep -E "(myID|startTime)" || true
|
||||
# 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 "Folders:"
|
||||
syncthing cli show config 2>/dev/null | jq -r '.folders[] | " \(.label // .id): \(.path)"' 2>/dev/null || echo " Unable to get folder info"
|
||||
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 " Syncthing not running"
|
||||
echo " (not running)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-all}" in
|
||||
# ============================================================================
|
||||
# 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)
|
||||
status|s)
|
||||
show_status
|
||||
;;
|
||||
all|"")
|
||||
sync|"")
|
||||
sync_nb
|
||||
echo ""
|
||||
sync_syncthing
|
||||
echo ""
|
||||
log_success "All sync operations complete"
|
||||
;;
|
||||
-h|--help|help)
|
||||
echo "usync - Unified sync command"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " usync Sync everything (nb + Syncthing)"
|
||||
echo " usync nb Sync only nb notebooks"
|
||||
echo " usync st Trigger Syncthing scan"
|
||||
echo " usync status Show sync status"
|
||||
echo " usync help Show this help"
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
|
||||
526
setup
Executable file
526
setup
Executable file
@@ -0,0 +1,526 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup - One command setup for Ultimate Notetaking System
|
||||
#
|
||||
# Usage:
|
||||
# ./setup # Interactive setup
|
||||
# ./setup <url> <age-key> # 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 '(?<=<device id=")[^"]+' "$cfg" 2>/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 <your-git-url> && git push -u origin main"
|
||||
echo ""
|
||||
echo " 2. On other devices, run:"
|
||||
echo " ./setup <git-url> <age-key>"
|
||||
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 <device-id> [name]"; exit 1; }
|
||||
add_device "$2" "${3:-}"
|
||||
;;
|
||||
help|-h|--help)
|
||||
cat <<'EOF'
|
||||
setup - One command setup
|
||||
|
||||
Usage:
|
||||
./setup Interactive setup
|
||||
./setup <url> <key> Join existing setup
|
||||
./setup mobile Pair mobile device
|
||||
./setup add <id> [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 <url> <key>
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
setup_join "$1" "$2"
|
||||
else
|
||||
err "Usage: ./setup <git-url> <age-key>"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user