bin/ 230 CLI tools (ask-*, br-*, agent-*, roadid, carpool) scripts/ 99 automation scripts fleet/ Node configs and deployment workers/ Cloudflare Worker sources (roadpay, road-search, squad webhooks) roadc/ RoadC programming language roadnet/ Mesh network (5 APs, WireGuard) operator/ Memory system scripts config/ System configs dotfiles/ Shell configs docs/ Documentation BlackRoad OS — Pave Tomorrow. RoadChain-SHA2048: d1a24f55318d338b RoadChain-Identity: alexa@sovereign RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
233 lines
7.4 KiB
Bash
Executable File
233 lines
7.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# ☎️ BlackRoad Local Ring — event-driven file sync
|
|
#
|
|
# Instead of dumb 30min cron, this WATCHES for changes and rings git
|
|
# when something moves. Debounces so rapid saves don't spam pushes.
|
|
#
|
|
# Usage:
|
|
# ~/local-ring.sh # Start the daemon (foreground)
|
|
# ~/local-ring.sh --daemon # Start backgrounded
|
|
# ~/local-ring.sh --stop # Kill running daemon
|
|
# ~/local-ring.sh --status # Check if running
|
|
#
|
|
# How it works:
|
|
# 1. fswatch monitors ~/ for file changes (filtered to what matters)
|
|
# 2. Changes accumulate in a buffer (debounce window)
|
|
# 3. After 10s of quiet, it "rings" git — stages, commits, pushes
|
|
# 4. Every 5min it also checks the Pis for anything they want to send
|
|
# 5. Logs everything to /tmp/local-ring.log
|
|
|
|
set -euo pipefail
|
|
|
|
PINK='\033[38;5;205m'
|
|
AMBER='\033[38;5;214m'
|
|
GREEN='\033[38;5;82m'
|
|
BLUE='\033[38;5;69m'
|
|
VIOLET='\033[38;5;135m'
|
|
RESET='\033[0m'
|
|
|
|
LOCAL_DIR="$HOME/local"
|
|
PIDFILE="/tmp/local-ring.pid"
|
|
LOGFILE="/tmp/local-ring.log"
|
|
DEBOUNCE=10 # seconds of quiet before pushing
|
|
FLEET_INTERVAL=300 # check fleet every 5 min
|
|
RING_COUNT=0
|
|
LAST_FLEET_CHECK=0
|
|
|
|
# ── helpers ──────────────────────────────────────────────
|
|
|
|
log() { echo -e "${PINK}[ring]${RESET} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE"; }
|
|
ring() { echo -e "${GREEN}☎️ RING${RESET} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE"; }
|
|
busy() { echo -e "${AMBER}📞 BUSY${RESET} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE"; }
|
|
drop() { echo -e "${VIOLET}📱 DROP${RESET} $(date '+%H:%M:%S') $1" | tee -a "$LOGFILE"; }
|
|
|
|
# ── daemon control ───────────────────────────────────────
|
|
|
|
stop_daemon() {
|
|
if [ -f "$PIDFILE" ]; then
|
|
pid=$(cat "$PIDFILE")
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
kill "$pid" 2>/dev/null
|
|
# kill fswatch children too
|
|
pkill -P "$pid" 2>/dev/null || true
|
|
rm -f "$PIDFILE"
|
|
log "Daemon stopped (pid $pid)"
|
|
return 0
|
|
fi
|
|
rm -f "$PIDFILE"
|
|
fi
|
|
log "No daemon running"
|
|
return 1
|
|
}
|
|
|
|
status_daemon() {
|
|
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
pid=$(cat "$PIDFILE")
|
|
uptime_s=$(( $(date +%s) - $(stat -f%m "$PIDFILE") ))
|
|
uptime_m=$(( uptime_s / 60 ))
|
|
echo -e "${GREEN}☎️ Ring daemon is UP${RESET} (pid $pid, ${uptime_m}m uptime, $RING_COUNT rings)"
|
|
tail -5 "$LOGFILE" 2>/dev/null
|
|
else
|
|
echo -e "${AMBER}☎️ Ring daemon is DOWN${RESET}"
|
|
fi
|
|
}
|
|
|
|
case "${1:-}" in
|
|
--stop) stop_daemon; exit ;;
|
|
--status) status_daemon; exit ;;
|
|
--daemon)
|
|
stop_daemon 2>/dev/null || true
|
|
nohup "$0" > /dev/null 2>&1 &
|
|
echo $! > "$PIDFILE"
|
|
log "Daemon started (pid $!)"
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
# ── directories to watch ─────────────────────────────────
|
|
|
|
WATCH_PATHS=(
|
|
"$HOME" # loose scripts
|
|
"$HOME/bin"
|
|
"$HOME/roadnet"
|
|
"$HOME/roadc"
|
|
"$HOME/config"
|
|
"$HOME/memory"
|
|
"$HOME/.claude"
|
|
)
|
|
|
|
# Filter: only care about these extensions
|
|
WATCH_FILTER='.*\.(sh|py|js|ts|json|yaml|yml|toml|html|css|md|txt|conf|modelfile)$'
|
|
|
|
# Ignore patterns for fswatch
|
|
FSWATCH_EXCLUDES=(
|
|
--exclude='\.git/'
|
|
--exclude='node_modules/'
|
|
--exclude='__pycache__/'
|
|
--exclude='\.next/'
|
|
--exclude='dist/'
|
|
--exclude='\.DS_Store'
|
|
--exclude='\.env'
|
|
--exclude='package-lock\.json'
|
|
--exclude='/local/' # don't watch our own sync dir
|
|
--exclude='blackroad-operator/' # has its own repo
|
|
)
|
|
|
|
# ── sync function ────────────────────────────────────────
|
|
|
|
do_sync() {
|
|
cd "$LOCAL_DIR" || return 1
|
|
|
|
# Run the existing sync script silently
|
|
"$HOME/local-sync.sh" > /dev/null 2>&1 && {
|
|
RING_COUNT=$((RING_COUNT + 1))
|
|
ring "Push #$RING_COUNT complete"
|
|
return 0
|
|
} || {
|
|
busy "Sync had no changes or failed"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# ── fleet check (ask the besties) ────────────────────────
|
|
|
|
check_fleet() {
|
|
local now
|
|
now=$(date +%s)
|
|
if (( now - LAST_FLEET_CHECK < FLEET_INTERVAL )); then
|
|
return
|
|
fi
|
|
LAST_FLEET_CHECK=$now
|
|
|
|
drop "Ringing the fleet..."
|
|
|
|
# Pull interesting files from Pis if they're up
|
|
for host in cecilia alice octavia; do
|
|
case $host in
|
|
cecilia) user="blackroad"; ip="192.168.4.96" ;;
|
|
alice) user="pi"; ip="192.168.4.49" ;;
|
|
octavia) user="pi"; ip="192.168.4.100" ;;
|
|
esac
|
|
|
|
if ssh -o ConnectTimeout=3 -o BatchMode=yes "$user@$ip" true 2>/dev/null; then
|
|
mkdir -p "$LOCAL_DIR/fleet/$host"
|
|
|
|
# Grab their crontabs
|
|
ssh -o ConnectTimeout=5 "$user@$ip" "crontab -l 2>/dev/null" \
|
|
> "$LOCAL_DIR/fleet/$host/crontab.txt" 2>/dev/null || true
|
|
|
|
# Grab their service list
|
|
ssh -o ConnectTimeout=5 "$user@$ip" "systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null | awk '{print \$1}'" \
|
|
> "$LOCAL_DIR/fleet/$host/services.txt" 2>/dev/null || true
|
|
|
|
# Grab key configs
|
|
ssh -o ConnectTimeout=5 "$user@$ip" "cat /etc/hostname 2>/dev/null" \
|
|
> "$LOCAL_DIR/fleet/$host/hostname.txt" 2>/dev/null || true
|
|
|
|
# Grab blackroad scripts if they exist
|
|
rsync -a --update --timeout=10 \
|
|
--exclude='.git/' --exclude='node_modules/' --exclude='__pycache__/' \
|
|
"$user@$ip:/opt/blackroad/" "$LOCAL_DIR/fleet/$host/opt-blackroad/" 2>/dev/null || true
|
|
|
|
drop "$host picked up — synced"
|
|
else
|
|
drop "$host didn't answer"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ── main loop ────────────────────────────────────────────
|
|
|
|
# Write PID if running in foreground
|
|
echo $$ > "$PIDFILE"
|
|
trap 'rm -f "$PIDFILE"; log "Ring daemon exiting"; exit 0' INT TERM
|
|
|
|
log "☎️ Ring daemon starting — watching ${#WATCH_PATHS[@]} paths"
|
|
log "Debounce: ${DEBOUNCE}s | Fleet check: every ${FLEET_INTERVAL}s"
|
|
|
|
# Initial sync on start
|
|
do_sync
|
|
|
|
# Start fswatch in the background, pipe events into the debounce loop
|
|
CHANGED=false
|
|
LAST_EVENT=0
|
|
|
|
fswatch -r \
|
|
--event Created --event Updated --event Renamed --event Removed \
|
|
-E --regex --include="$WATCH_FILTER" \
|
|
"${FSWATCH_EXCLUDES[@]}" \
|
|
"${WATCH_PATHS[@]}" 2>/dev/null | while read -r event; do
|
|
|
|
CHANGED=true
|
|
LAST_EVENT=$(date +%s)
|
|
|
|
# Non-blocking debounce: check if enough quiet time has passed
|
|
# We use a subshell timeout approach
|
|
(
|
|
sleep "$DEBOUNCE"
|
|
# After sleeping, check if we're still the latest event
|
|
now=$(date +%s)
|
|
if (( now - LAST_EVENT >= DEBOUNCE )); then
|
|
do_sync
|
|
check_fleet
|
|
fi
|
|
) &
|
|
|
|
done &
|
|
|
|
FSWATCH_PID=$!
|
|
|
|
# Keepalive loop — also handles fleet checks on interval
|
|
while true; do
|
|
sleep 60
|
|
|
|
# Fleet check on interval even without file changes
|
|
check_fleet
|
|
|
|
# Safety net: if fswatch died, restart it
|
|
if ! kill -0 "$FSWATCH_PID" 2>/dev/null; then
|
|
log "fswatch died, restarting..."
|
|
exec "$0"
|
|
fi
|
|
done
|