#!/usr/bin/env bash # Install a systemd *user* timer that runs `ai-chat-exporter sync` on a schedule. # # ./scheduling/install-systemd-timer.sh --provider claude-code --provider codex # ./scheduling/install-systemd-timer.sh --provider all --time 21:30 # ./scheduling/install-systemd-timer.sh --uninstall # # User units (not system units) are the right scope: the archive is per-user, # the .env holds that user's session tokens, and Joplin runs in their session. # # --provider is repeatable. The CLI takes one provider per run, so each one # becomes its own ExecStart line, executed in order. Prefer listing only the # providers that actually work unattended on this machine — a web provider with # an expired session token will fail the unit every single day, which trains you # to ignore the failure you actually want to see. set -euo pipefail REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" UNIT_DIR="$HOME/.config/systemd/user" NAME="aichat-sync" TIME="09:00" PROVIDERS=() UNINSTALL=0 while [ $# -gt 0 ]; do case "$1" in --provider) PROVIDERS+=("$2"); shift 2 ;; --time) TIME="$2"; shift 2 ;; --name) NAME="$2"; shift 2 ;; --uninstall) UNINSTALL=1; shift ;; -h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}"; exit 0 ;; *) echo "unknown argument: $1" >&2; exit 1 ;; esac done if [ "$UNINSTALL" -eq 1 ]; then systemctl --user disable --now "$NAME.timer" 2>/dev/null || true rm -f "$UNIT_DIR/$NAME.timer" "$UNIT_DIR/$NAME.service" systemctl --user daemon-reload echo "Removed $NAME.timer and $NAME.service." exit 0 fi [ ${#PROVIDERS[@]} -eq 0 ] && PROVIDERS=("all") for f in "$REPO/ai-chat-exporter" "$REPO/scheduling/run-sync.sh"; do if [ ! -x "$f" ]; then echo "error: $f is missing or not executable." >&2 exit 1 fi done mkdir -p "$UNIT_DIR" # WorkingDirectory is the point of the whole unit: .env, cache/ and exports/ all # resolve against it, so the scheduled run writes to the same archive an # interactive run from this directory would. { echo "[Unit]" echo "Description=AI chat archive sync" echo "Documentation=file://$REPO/README.md" echo "After=network-online.target" echo "Wants=network-online.target" echo echo "[Service]" echo "Type=oneshot" echo "WorkingDirectory=$REPO" echo "Environment=AI_CHAT_EXPORTER_QUIET_CWD=1" echo "Environment=AICHAT_SYNC_UNIT=$NAME" # run-sync.sh attempts every provider even after one fails, and pushes a # FAILED notification for any run that died without sending its own. echo "ExecStart=$REPO/scheduling/run-sync.sh ${PROVIDERS[*]}" } > "$UNIT_DIR/$NAME.service" # Persistent=true runs a missed schedule at the next boot — the machine being # off at 09:00 should delay the archive, not skip it. RandomizedDelaySec keeps # the web providers from being hit at exactly the same second every day. cat > "$UNIT_DIR/$NAME.timer" <