Initial fleet inventory + automation

inventory.yml: 6-host source of truth (quantum, ghost, smith, architect, tank, morpheus)
fleet.sh: status / update / restart / exec / deploy / pair / sync-usage / list / ssh
configs/tmux.conf: canonical mobile-tuned config pushed by 'fleet deploy tmux'

All multi-host ops run in parallel with grouped output; offline hosts are
reported but don't halt the sweep. macOS uses brew_services + brew upgrade;
linux uses systemd_user + getmoshi.app install.sh.
This commit is contained in:
quantum
2026-05-30 19:07:23 -07:00
commit d04fe66910
4 changed files with 406 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Never commit pairing tokens.
*.token
.env
.env.*
secrets/
# Local-only scratch
*.log
.DS_Store
+16
View File
@@ -0,0 +1,16 @@
# Canonical tmux.conf pushed to every fleet host by `fleet deploy tmux`.
# Mobile-tuned: larger scrollback, mouse, finer wheel scroll for phone use.
set -g history-limit 100000
set -g mouse on
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g set-titles on
set -g set-titles-string "#I: #T"
# finer mobile wheel scrolling (default 5 lines is jumpy on a phone)
bind -T root WheelUpPane send-keys -X -N 3 scroll-up
bind -T root WheelDownPane send-keys -X -N 3 scroll-down
bind -T copy-mode-vi WheelUpPane send-keys -X -N 3 scroll-up
bind -T copy-mode-vi WheelDownPane send-keys -X -N 3 scroll-down
Executable
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env bash
# fleet.sh — Moshi fleet inventory + automation.
#
# fleet status Per-host: reachable, moshi-hook version, daemon, paired.
# fleet update [host...] Upgrade moshi-hook (brew on macOS, install.sh on linux) + restart daemon.
# fleet restart [host...] Restart moshi-hook daemon.
# fleet exec '<cmd>' [host...] Run a shell command on each host; output grouped per-host.
# fleet deploy tmux [host...] Push configs/tmux.conf to ~/.tmux.conf and reload running tmux servers.
# fleet pair <token> <host> Pair a host using its secret_store from inventory.
# fleet sync-usage [host...] Force `moshi-hook usage --sync` on each host (Usage tab refresh).
# fleet list Print inventory as a table.
# fleet ssh <host> Open an interactive ssh to a host.
#
# Notes:
# - Without [host...] filter, runs on ALL hosts in inventory.
# - All multi-host operations run in parallel; output is grouped per-host.
# - Offline hosts are reported as such; they don't halt the sweep.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INVENTORY="$SCRIPT_DIR/inventory.yml"
CONFIGS="$SCRIPT_DIR/configs"
SSH_OPTS=(-o ConnectTimeout=8 -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
# ---------- helpers ----------
die() { printf 'fleet: %s\n' "$*" >&2; exit 1; }
require() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
require yq
require ssh
all_hosts() { yq -r '.hosts[].name' "$INVENTORY"; }
field() {
local host=$1 key=$2
yq -r ".hosts[] | select(.name == \"$host\") | .${key}" "$INVENTORY"
}
# Resolve host list: if any args, use them; else all.
resolve_hosts() {
if [ $# -eq 0 ]; then
all_hosts
else
for h in "$@"; do
if ! yq -e ".hosts[] | select(.name == \"$h\")" "$INVENTORY" >/dev/null 2>&1; then
die "unknown host: $h"
fi
echo "$h"
done
fi
}
# Build env-prefix and binary path appropriate for the host's OS.
remote_env_prefix() {
local host=$1 os
os=$(field "$host" os)
if [ "$os" = "linux" ]; then
# systemctl --user over SSH needs XDG_RUNTIME_DIR exported.
echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u); '
else
# On macOS, source ~/.zshenv so brew's PATH is set under non-interactive SSH.
echo 'source ~/.zshenv 2>/dev/null; '
fi
}
# Run a command on a host. Honours is_local (skips ssh).
ssh_on() {
local host=$1; shift
local cmd=$1
local user ip is_local prefix
user=$(field "$host" user)
ip=$(field "$host" ts_ip)
is_local=$(field "$host" is_local)
prefix=$(remote_env_prefix "$host")
if [ "$is_local" = "true" ]; then
bash -c "${prefix}${cmd}"
else
ssh "${SSH_OPTS[@]}" "$user@$ip" "${prefix}${cmd}"
fi
}
# Parallel-run a function-of-one-host against many hosts; print grouped output.
parallel_run() {
local fn=$1; shift
local hosts=("$@")
local tmpdir; tmpdir=$(mktemp -d)
local pids=()
for h in "${hosts[@]}"; do
( "$fn" "$h" >"$tmpdir/$h.out" 2>&1; echo $? >"$tmpdir/$h.rc" ) &
pids+=($!)
done
for p in "${pids[@]}"; do wait "$p" 2>/dev/null || true; done
for h in "${hosts[@]}"; do
printf '\n\033[1;36m=== %s ===\033[0m\n' "$h"
cat "$tmpdir/$h.out"
done
rm -rf "$tmpdir"
}
# ---------- subcommands ----------
cmd_list() {
printf '%-12s %-10s %-18s %-8s %-9s %-15s %-13s\n' \
NAME USER TS_IP OS ARCH DAEMON STORE
printf '%-12s %-10s %-18s %-8s %-9s %-15s %-13s\n' \
------------ ---------- ------------------ -------- --------- --------------- -------------
for h in $(all_hosts); do
printf '%-12s %-10s %-18s %-8s %-9s %-15s %-13s\n' \
"$h" \
"$(field "$h" user)" \
"$(field "$h" ts_ip)" \
"$(field "$h" os)" \
"$(field "$h" arch)" \
"$(field "$h" daemon)" \
"$(field "$h" secret_store)"
done
}
status_one() {
local host=$1
local hook; hook=$(field "$host" hook_bin)
local daemon; daemon=$(field "$host" daemon)
local daemon_check
if [ "$daemon" = "brew_services" ]; then
daemon_check='brew services list 2>/dev/null | awk "\$1==\"moshi-hook\"{print \$2}"'
else
daemon_check='systemctl --user is-active moshi-hook 2>/dev/null'
fi
ssh_on "$host" "
if ! command -v ssh >/dev/null; then :; fi
if [ ! -x '$hook' ]; then echo 'moshi-hook: NOT INSTALLED at $hook'; exit 0; fi
ver=\$('$hook' --version 2>/dev/null || echo 'pre-0.2 (no --version flag)')
paired=\$('$hook' status 2>/dev/null | awk '/^status:/{print \$2}')
display=\$('$hook' status 2>/dev/null | awk -F': *' '/^display name:/{print \$2}')
host_id=\$('$hook' status 2>/dev/null | awk '/^host id:/{print \$3}')
daemon=\$($daemon_check)
printf 'version: %s\n' \"\$ver\"
printf 'daemon: %s\n' \"\${daemon:-unknown}\"
printf 'paired: %s\n' \"\${paired:-unknown}\"
printf 'display: %s\n' \"\${display:-?}\"
printf 'host_id: %s\n' \"\${host_id:-?}\"
" || echo "UNREACHABLE (offline / ssh refused / timeout)"
}
cmd_status() {
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
parallel_run status_one "${hosts[@]}"
}
update_one() {
local host=$1
local os; os=$(field "$host" os)
local daemon; daemon=$(field "$host" daemon)
local hook; hook=$(field "$host" hook_bin)
if [ "$os" = "macos" ]; then
ssh_on "$host" "
brew update >/dev/null 2>&1
brew upgrade moshi-hook 2>&1 | tail -4
brew services restart moshi-hook 2>&1 | tail -1
sleep 2
'$hook' --version 2>&1
" || echo "UNREACHABLE"
else
ssh_on "$host" "
curl -fsSL https://getmoshi.app/install.sh | sh 2>&1 | tail -3
systemctl --user restart moshi-hook 2>&1
sleep 2
'$hook' --version 2>&1
systemctl --user is-active moshi-hook
" || echo "UNREACHABLE"
fi
}
cmd_update() {
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
parallel_run update_one "${hosts[@]}"
}
restart_one() {
local host=$1
local daemon; daemon=$(field "$host" daemon)
if [ "$daemon" = "brew_services" ]; then
ssh_on "$host" "brew services restart moshi-hook 2>&1 | tail -1" || echo "UNREACHABLE"
else
ssh_on "$host" "systemctl --user restart moshi-hook && systemctl --user is-active moshi-hook" || echo "UNREACHABLE"
fi
}
cmd_restart() {
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
parallel_run restart_one "${hosts[@]}"
}
sync_one() {
local host=$1
local hook; hook=$(field "$host" hook_bin)
ssh_on "$host" "'$hook' usage --sync 2>&1 | tail -2" || echo "UNREACHABLE"
}
cmd_sync_usage() {
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
parallel_run sync_one "${hosts[@]}"
}
# ad-hoc exec: first arg is the command, the rest are host filters
EXEC_CMD=""
exec_one() {
local host=$1
ssh_on "$host" "$EXEC_CMD" || echo "UNREACHABLE"
}
cmd_exec() {
[ $# -ge 1 ] || die "usage: fleet exec '<command>' [host...]"
EXEC_CMD=$1; shift
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
parallel_run exec_one "${hosts[@]}"
}
# deploy: only "tmux" supported for now
deploy_tmux_one() {
local host=$1
local user ip is_local
user=$(field "$host" user)
ip=$(field "$host" ts_ip)
is_local=$(field "$host" is_local)
if [ "$is_local" = "true" ]; then
cp "$CONFIGS/tmux.conf" ~/.tmux.conf
else
scp "${SSH_OPTS[@]}" -q "$CONFIGS/tmux.conf" "$user@$ip:~/.tmux.conf" || { echo "UNREACHABLE"; return; }
fi
# reload any running tmux server (no-op if none)
ssh_on "$host" "tmux ls >/dev/null 2>&1 && tmux source-file ~/.tmux.conf 2>&1 && echo 'reloaded running tmux' || echo 'no running tmux server (config still installed)'"
}
cmd_deploy() {
case "${1:-}" in
tmux)
shift
local hosts=(); while IFS= read -r h; do hosts+=("$h"); done < <(resolve_hosts "$@")
parallel_run deploy_tmux_one "${hosts[@]}"
;;
*) die "usage: fleet deploy tmux [host...]" ;;
esac
}
cmd_pair() {
[ $# -eq 2 ] || die "usage: fleet pair <token> <host>"
local token=$1 host=$2
local hook store
hook=$(field "$host" hook_bin)
store=$(field "$host" secret_store)
printf '\n\033[1;36m=== %s ===\033[0m\n' "$host"
ssh_on "$host" "'$hook' pair --token '$token' --store '$store' 2>&1; '$hook' install 2>&1 | tail -5"
printf 'restarting daemon...\n'
restart_one "$host"
}
cmd_ssh() {
[ $# -eq 1 ] || die "usage: fleet ssh <host>"
local host=$1
local user ip; user=$(field "$host" user); ip=$(field "$host" ts_ip)
exec ssh "$user@$ip"
}
# ---------- dispatch ----------
usage() {
sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
}
case "${1:-status}" in
status) shift; cmd_status "$@" ;;
update) shift; cmd_update "$@" ;;
restart) shift; cmd_restart "$@" ;;
exec) shift; cmd_exec "$@" ;;
deploy) shift; cmd_deploy "$@" ;;
pair) shift; cmd_pair "$@" ;;
sync-usage) shift; cmd_sync_usage "$@" ;;
list) shift; cmd_list ;;
ssh) shift; cmd_ssh "$@" ;;
-h|--help|help) usage ;;
*) usage; exit 1 ;;
esac
+98
View File
@@ -0,0 +1,98 @@
# Moshi remote-agent fleet inventory.
#
# Source of truth for the fleet.sh tooling. Edit this when adding/removing
# hosts or changing their bootstrap details. Live state (online/offline,
# moshi-hook version, daemon up) is queried at runtime — do NOT cache it here.
#
# Fields:
# name short name (used as fleet.sh argument and ssh display)
# user ssh username on the host
# ts_ip Tailscale IP (preferred over MagicDNS — survives DNS hiccups)
# ts_name Tailscale MagicDNS short name (used by Easy Pair QR)
# os macos | linux
# arch arm64 | x86_64
# brew_prefix /opt/homebrew | /usr/local | "" (linux)
# hook_bin absolute path to moshi-hook on that host
# daemon brew_services | systemd_user
# secret_store keychain | file (moshi-hook --store)
# is_local true on the host running fleet.sh; skips ssh
# notes free-form
hosts:
- name: quantum
user: quantum
ts_ip: 100.90.165.125
ts_name: quantum
os: macos
arch: arm64
brew_prefix: /opt/homebrew
hook_bin: /opt/homebrew/bin/moshi-hook
daemon: brew_services
secret_store: keychain
is_local: true
notes: "M5 MacBook Pro — control plane host"
- name: ghost
user: osobh
ts_ip: 100.79.33.78
ts_name: ghost
os: macos
arch: arm64
brew_prefix: /opt/homebrew
hook_bin: /opt/homebrew/bin/moshi-hook
daemon: brew_services
secret_store: file
is_local: false
notes: "MBP arm64 — pair with --store file (keychain locks over SSH)"
- name: smith
user: builder
ts_ip: 100.127.18.60
ts_name: smith
os: macos
arch: x86_64
brew_prefix: /usr/local
hook_bin: /usr/local/bin/moshi-hook
daemon: brew_services
secret_store: file
is_local: false
notes: "Intel MBP — pair with --store file"
- name: architect
user: osobh
ts_ip: 100.104.171.32
ts_name: architect
os: linux
arch: x86_64
brew_prefix: ""
hook_bin: /home/osobh/.local/bin/moshi-hook
daemon: systemd_user
secret_store: file
is_local: false
notes: "Linux — needs XDG_RUNTIME_DIR=/run/user/$(id -u) over SSH"
- name: tank
user: osobh
ts_ip: 100.108.129.81
ts_name: tank
os: linux
arch: x86_64
brew_prefix: ""
hook_bin: /home/osobh/.local/bin/moshi-hook
daemon: systemd_user
secret_store: file
is_local: false
notes: ""
- name: morpheus
user: osobh
ts_ip: 100.123.224.84
ts_name: morpheus
os: linux
arch: x86_64
brew_prefix: ""
hook_bin: /home/osobh/.local/bin/moshi-hook
daemon: systemd_user
secret_store: file
is_local: false
notes: ""