aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/cmail-setup-finish.sh56
-rwxr-xr-xscripts/post-rebuild-check79
-rwxr-xr-xscripts/zz-bluetooth-resume86
3 files changed, 197 insertions, 24 deletions
diff --git a/scripts/cmail-setup-finish.sh b/scripts/cmail-setup-finish.sh
index 949023f..8c27eda 100755
--- a/scripts/cmail-setup-finish.sh
+++ b/scripts/cmail-setup-finish.sh
@@ -1,32 +1,37 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
-# cmail-setup-finish.sh — finish Proton Mail Bridge + cmail-action setup after
-# Bridge first-run. Idempotent; safe to re-run after a Bridge cert rotation or
-# a claude-templates re-clone.
+# cmail-setup-finish.sh — finish Proton Mail Bridge setup after Bridge
+# first-run. Idempotent; safe to re-run after a Bridge cert rotation.
#
# Pre-reqs (the script aborts if any are missing):
# - protonmail-bridge installed (archsetup handles it)
# - You have run 'protonmail-bridge --cli', logged in, and quit at least once
# (the script looks for state at ~/.config/protonmail/bridge-v3/)
-# - claude-templates cloned at ~/projects/claude-templates
# - dotfiles stowed (~/.config/.cmailpass.gpg present)
#
+# Not a pre-req, but checked and warned about: cmail-action on PATH. rulesets'
+# `make install` links it, and session start runs that, so on a machine that
+# runs agent sessions it arrives without anyone asking. On one that doesn't,
+# it needs the command by hand. The script never invokes it either way.
+#
# What it does:
# 1. Decrypts ~/.config/.cmailpass.gpg → ~/.config/.cmailpass (mode 0600)
# 2. Copies Bridge's self-signed cert → ~/.config/protonbridge.pem
-# 3. Symlinks ~/projects/claude-templates/.ai/scripts/cmail-action.py
-# → ~/.local/bin/cmail-action
-# 4. Removes the leftover ~/.config/autostart/Proton Mail Bridge.desktop
+# 3. Removes the leftover ~/.config/autostart/Proton Mail Bridge.desktop
# stub (it double-launches Bridge alongside the systemd user service
# and throws an "orphan instance" dialog every login)
-# 5. Installs a wait-for-dns drop-in so Bridge doesn't spam
+# 4. Installs a wait-for-dns drop-in so Bridge doesn't spam
# name-resolution errors during the early-boot DNS race
-# 6. Enables + starts the protonmail-bridge user service
-# 7. Verifies Bridge is listening on 127.0.0.1:1143 / :1025
+# 5. Enables + starts the protonmail-bridge user service
+# 6. Verifies Bridge is listening on 127.0.0.1:1143 / :1025
+#
+# It no longer installs cmail-action. That moved to rulesets
+# (claude-templates/bin/), whose `make install` owns the symlink.
set -euo pipefail
err() { printf 'error: %s\n' "$*" >&2; exit 1; }
+warn() { printf 'warning: %s\n' "$*" >&2; }
info() { printf '==> %s\n' "$*"; }
ok() { printf ' %s\n' "$*"; }
@@ -47,9 +52,20 @@ bridge_state="$HOME/.config/protonmail/bridge-v3"
[ -d "$bridge_state" ] \
|| err "Bridge has no state at $bridge_state — run 'protonmail-bridge --cli' and log in first"
-cmail_action_src="$HOME/projects/claude-templates/.ai/scripts/cmail-action.py"
-[ -f "$cmail_action_src" ] \
- || err "cmail-action.py not found at $cmail_action_src — clone claude-templates first"
+# cmail-action is no longer this script's to install. It lives in rulesets at
+# claude-templates/bin/, and rulesets' `make install` links everything there
+# into ~/.local/bin. Session start runs that, so on a machine that runs agent
+# sessions the symlink arrives on its own; on one that doesn't, it needs the
+# command below.
+#
+# A warning rather than an abort, because this script never invokes the tool.
+# Its job is to leave Bridge working, and it can finish that whether or not a
+# mail client has been linked yet. Aborting here would make Bridge setup
+# depend on rulesets being cloned and installed first, an ordering neither
+# repo otherwise needs, and would strand a fresh machine with Bridge ready and
+# the script refusing to configure it.
+command -v cmail-action >/dev/null 2>&1 \
+ || warn "cmail-action not on PATH — run 'make -C ~/code/rulesets install' before sending mail"
cmailpass_enc="$HOME/.config/.cmailpass.gpg"
[ -f "$cmailpass_enc" ] \
@@ -69,13 +85,7 @@ cert_dst="$HOME/.config/protonbridge.pem"
cp "$cert_src" "$cert_dst"
ok "copied $cert_src → $cert_dst"
-# 4. Symlink cmail-action
-info "symlinking cmail-action"
-mkdir -p "$HOME/.local/bin"
-ln -sf "$cmail_action_src" "$HOME/.local/bin/cmail-action"
-ok "linked $HOME/.local/bin/cmail-action → $cmail_action_src"
-
-# 5. Remove leftover XDG autostart stub
+# 4. Remove leftover XDG autostart stub
# The systemd --user service is the canonical launcher. The autostart .desktop
# starts a second Bridge instance that can't get the lock and pops up an
# "orphan instance" dialog every login.
@@ -88,7 +98,7 @@ else
ok "no autostart stub present"
fi
-# 6. Install wait-for-dns drop-in
+# 5. Install wait-for-dns drop-in
# User-instance systemd doesn't carry network-online.target / nss-lookup.target,
# so the packaged unit's After=network.target doesn't imply DNS readiness.
# Bridge starts before the resolver is up and its first API calls all fail
@@ -107,7 +117,7 @@ ok "wrote $dropin_file"
systemctl --user daemon-reload
ok "reloaded systemd user units"
-# 7. Enable + start systemd user service
+# 6. Enable + start systemd user service
info "enabling protonmail-bridge user service"
was_active=0
systemctl --user is-active --quiet protonmail-bridge.service && was_active=1
@@ -119,7 +129,7 @@ else
ok "service active"
fi
-# 8. Verify
+# 7. Verify
info "verifying Bridge is listening"
listening="$(ss -ltn 2>/dev/null || true)"
missing=""
diff --git a/scripts/post-rebuild-check b/scripts/post-rebuild-check
index 14f2415..aa7ef83 100755
--- a/scripts/post-rebuild-check
+++ b/scripts/post-rebuild-check
@@ -72,6 +72,15 @@
# running, the special value MISSING = not installed
# PRC_REPO_REMOTES newline list of "path origin-url"; an empty URL
# means origin could not be read
+# PRC_UNITS_EXPECTED_DISABLED
+# newline list of units whose not-enabled state is
+# deliberate here, replacing the file below
+# PRC_UNITS_EXPECTED_DISABLED_FILE
+# path to that list (default:
+# $XDG_CONFIG_HOME/post-rebuild-check/units-expected-disabled).
+# One unit per line, # starts a comment. Machine-local
+# on purpose: the same unit is correctly enabled on one
+# box and not another
# PRC_SYSTEMCTL path to the systemctl binary (a fake, under test)
# PRC_SYSTEMCTL_TIMEOUT seconds to allow each systemctl call (default 5)
#
@@ -209,6 +218,39 @@ done < "$STAGE"
report "check 1/8: failed units"
# --- 2. user unit files present but not enabled ---------------------------
+#
+# Units nothing intends to enable here are read from a machine-local list.
+# "Enabled" is this check's proxy for "will actually run", and the proxy is
+# wrong for a unit nobody means to enable on this box. velox carries four, for
+# four different reasons: geoclue-agent is redundant because hyprland's
+# exec-once starts the binary directly, emacs is started on demand by
+# emacsclient, obs-record-watchdog only matters while recording, and
+# obsbot-wb-guard needs an OBSBOT the machine does not have. Left unexempted
+# they report at every run, and four permanent lines in front of every real one
+# teach you to skim the output -- the same argument check 4 makes about
+# CLAUDE.md.
+#
+# Machine-local rather than a marker in the shared unit file, because
+# obsbot-wb-guard is correctly ENABLED on ratio. One unit, a different right
+# answer per machine, so the shared file cannot hold the answer.
+#
+# An entry that turns out to be enabled after all is still a finding. Without
+# that the list rots into somewhere real findings go to die, which is worse
+# than the noise it removes.
+
+EXPECT_DISABLED_FILE="${PRC_UNITS_EXPECTED_DISABLED_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/post-rebuild-check/units-expected-disabled}"
+if [ -n "${PRC_UNITS_EXPECTED_DISABLED+set}" ]; then
+ expect_disabled=$PRC_UNITS_EXPECTED_DISABLED
+elif [ -f "$EXPECT_DISABLED_FILE" ]; then
+ expect_disabled=$(cat "$EXPECT_DISABLED_FILE" 2>/dev/null)
+else
+ expect_disabled=""
+fi
+# Strip comments and blanks once, here, so the membership test below is a
+# plain word match. The reason a unit is exempt is the most useful thing about
+# the entry, so the format has to carry one.
+expect_disabled=$(printf '%s\n' "$expect_disabled" \
+ | sed 's/#.*//' | awk 'NF {print $1}')
if [ -n "${PRC_UNIT_STATES+set}" ]; then
states=$PRC_UNIT_STATES
@@ -294,8 +336,34 @@ while read -r name state; do
esac
;;
esac
+ # Deliberately not enabled on this machine. Checked last, so it suppresses
+ # only this finding and never the dangling-link one decided above on the
+ # filesystem.
+ case "
+$expect_disabled
+" in
+ *"
+$name
+"*) continue ;;
+ esac
finding "unit file present but not enabled: $name ($state)"
done < "$STAGE"
+# The exemption list, checked in the other direction. An entry whose unit is
+# enabled after all suppresses nothing, and leaving it there is how the list
+# turns into a place real findings go to die. The loop above cannot catch this:
+# it skips any state that is not disabled or linked, so an enabled unit never
+# reaches it.
+printf '%s\n' "$expect_disabled" > "$WORK/expect" 2>/dev/null || {
+ echo "post-rebuild-check: cannot write $WORK/expect" >&2
+ echo " nothing was checked; this is not a pass" >&2; exit 1; }
+while IFS= read -r name; do
+ [ -n "$name" ] || continue
+ estate=$(awk -v u="$name" '$1 == u {print $2; exit}' "$WORK/states")
+ case "$estate" in
+ enabled|enabled-runtime)
+ finding "$name is listed as expected-disabled but is $estate — drop the stale exemption" ;;
+ esac
+done < "$WORK/expect"
report "check 2/8: unit files"
# --- 3. *.example files whose real sibling is missing ---------------------
@@ -360,6 +428,16 @@ printf '%s\n' "$projects" > "$WORK/projects" 2>/dev/null || {
# steady state rather than reinstall drift, and flagging it would put nine
# standing findings in front of every real one.
#
+# .claude/ is absent for the same reason and proven the same way. The
+# bootstrap and the gitignore sweep write it into the ignore set of every
+# gitignore-mode project whether or not one ever exists there, so the entry is
+# aspirational rather than a promise -- pearl, rsyncshot and yt-sync each name
+# it and none of the three has ever had one, on velox or on ratio. Dropping it
+# loses no real signal either: a project that genuinely carries a .claude/
+# (rules and hooks from a language bundle) has it re-synced by
+# sync-language-bundle.sh at every session start, so a true absence heals
+# itself before this check would run.
+#
# The list is fed to the inner loop straight from a heredoc rather than
# staged through a file. It is a constant, so a file bought nothing and cost
# a fifth unguarded write: had it failed (a full tmpfs, say) the inner loop
@@ -390,7 +468,6 @@ while IFS= read -r proj; do
esac
done <<'EOF'
.ai \.ai
-.claude \.claude
todo.org todo\.org
inbox inbox
EOF
diff --git a/scripts/zz-bluetooth-resume b/scripts/zz-bluetooth-resume
new file mode 100755
index 0000000..4273339
--- /dev/null
+++ b/scripts/zz-bluetooth-resume
@@ -0,0 +1,86 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-3.0-or-later
+# zz-bluetooth-resume - put bluetooth back after a sleep cycle.
+#
+# A systemd-sleep hook. Two things break bluetooth across sleep on a TLP
+# laptop, and nothing else on the machine fixes either one.
+#
+# 1. The rfkill soft-block is not restored. systemd-rfkill would do it, and
+# it is masked here deliberately -- it fights TLP's radio handling, so
+# configure_tlp_power masks it and TLP owns radios instead. TLP's own
+# sleep hook runs `tlp resume`, but its setting is
+# DEVICES_TO_ENABLE_ON_STARTUP: startup, not resume. TLP has no ON_RESUME
+# at all, so the resume edge has no owner. WiFi survives only because
+# NetworkManager unblocks itself; bluetooth has no equivalent.
+#
+# 2. The controller comes back wedged from a hibernate. It reports powered
+# and unblocked while scanning finds nothing whatever -- zero devices
+# where the same room gave seventeen a minute later -- and bluetoothd
+# logs "Failed to set mode" and "Failed to add device <mac>" at the
+# instant of resume. Reloading btusb clears it.
+#
+# Both observed on velox 2026-08-21, on the first suspend-then-hibernate cycle
+# after hibernate was switched back on. The second symptom is why unblocking
+# alone is not enough: rfkill was cleared by hand and scanning still returned
+# nothing until the driver was reloaded.
+#
+# The hook re-asserts TLP's own declared intent rather than inventing a policy.
+# A machine whose TLP config does not ask for bluetooth keeps it off, which is
+# what stops this from overriding a deliberate block at every wakeup.
+#
+# The zz- prefix orders it after TLP's own hook, so `tlp resume` has finished
+# before this runs.
+#
+# Test seams: BTR_RFKILL, BTR_MODPROBE, BTR_TLP_CONF, BTR_TLP_CONF_DIR,
+# BTR_SETTLE (seconds to wait between driver unload and load).
+
+set -u
+
+RFKILL="${BTR_RFKILL:-rfkill}"
+MODPROBE="${BTR_MODPROBE:-modprobe}"
+TLP_CONF="${BTR_TLP_CONF:-/etc/tlp.conf}"
+TLP_CONF_DIR="${BTR_TLP_CONF_DIR:-/etc/tlp.d}"
+SETTLE="${BTR_SETTLE:-1}"
+
+# post only. The pre phase has nothing to do, and acting there would fight the
+# suspend it is about to run.
+[ "${1:-}" = "post" ] || exit 0
+
+# Does TLP ask for bluetooth on this machine? Comments are stripped first, so a
+# commented-out example in the stock config cannot be read as a policy. Both
+# the main file and any drop-in count, and the last assignment wins the same
+# way TLP itself resolves them.
+wants_bluetooth() {
+ cat "$TLP_CONF" "$TLP_CONF_DIR"/*.conf 2>/dev/null \
+ | sed 's/#.*//' \
+ | awk -F= '/DEVICES_TO_ENABLE_ON_STARTUP/ { v = $2 } END { print v }' \
+ | tr -d '"' \
+ | tr ' ' '\n' \
+ | grep -qx "bluetooth"
+}
+
+wants_bluetooth || exit 0
+
+# The wedge follows a hibernate, which reinitialises the controller from a
+# saved image. A plain suspend brings USB back intact, so reloading there would
+# tear down a working adapter for nothing.
+#
+# suspend-then-hibernate reports that name whether or not it reached the
+# hibernate stage, so this reloads on a cycle that only suspended. That is the
+# cheap side of the trade: a couple of seconds against an adapter that answers
+# nothing until someone notices and reloads it by hand.
+case "${2:-}" in
+ hibernate|suspend-then-hibernate)
+ "$MODPROBE" -r btusb 2>/dev/null || true
+ [ "$SETTLE" = "0" ] || sleep "$SETTLE"
+ "$MODPROBE" btusb 2>/dev/null || true
+ ;;
+esac
+
+# After the reload, not before: a freshly loaded btusb can come up soft-blocked
+# and would undo an earlier unblock.
+"$RFKILL" unblock bluetooth 2>/dev/null || true
+
+# Never fail. systemd-sleep logs a failing hook, and that noise outlives the
+# cause it describes; nothing here is worth alarming a resume over.
+exit 0