110 lines
3.9 KiB
Bash
Executable File
110 lines
3.9 KiB
Bash
Executable File
#!/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")
|
|
|
|
if [ ! -x "$REPO/ai-chat-exporter" ]; then
|
|
echo "error: $REPO/ai-chat-exporter is missing or not executable." >&2
|
|
exit 1
|
|
fi
|
|
|
|
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"
|
|
# One ExecStart per provider would stop at the first failure, silently
|
|
# skipping the rest — an expired ChatGPT token would mean codex never runs.
|
|
# Loop instead, so every provider is attempted and the unit still reports
|
|
# failure if any of them failed.
|
|
printf 'ExecStart=/bin/sh -c '\''rc=0; for p in %s; do "$0" sync --provider "$p" --joplin-optional || rc=1; done; exit $rc'\'' %s\n' \
|
|
"${PROVIDERS[*]}" "$REPO/ai-chat-exporter"
|
|
} > "$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" <<EOF
|
|
[Unit]
|
|
Description=Run the AI chat archive sync daily
|
|
|
|
[Timer]
|
|
OnCalendar=*-*-* $TIME:00
|
|
Persistent=true
|
|
RandomizedDelaySec=300
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now "$NAME.timer"
|
|
|
|
echo "Installed $NAME.timer — daily at $TIME for: ${PROVIDERS[*]}"
|
|
echo
|
|
systemctl --user list-timers "$NAME.timer" --no-pager || true
|
|
cat <<EOF
|
|
|
|
Next steps:
|
|
• Timers only run while you have a session. To archive when logged out:
|
|
loginctl enable-linger $USER
|
|
• Run it once by hand to confirm:
|
|
systemctl --user start $NAME.service
|
|
• Read the log:
|
|
journalctl --user -u $NAME.service -n 50
|
|
• The terms-of-service notice must have been acknowledged interactively at
|
|
least once on this machine, or the unit exits 1 with an explanation.
|
|
EOF
|