feat: add sysupdate TUI script with Arch news + AI analysis
One-by-one pacman/AUR/flatpak updater with a dialog-based package selector, Arch RSS news matching per package, optional --AI mode that sends news to Claude and generates a timestamped /updatescript-<ts>.sh, and kernel-based baseline detection when /updatestate doesn't exist yet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
270aa841cf
commit
92e1924526
|
|
@ -0,0 +1,684 @@
|
|||
#!/usr/bin/env bash
|
||||
# sysupdate — Arch Linux System Update TUI
|
||||
# Usage: sysupdate [--AI]
|
||||
# Deps: yay, pacman-contrib (checkupdates), curl, python3, dialog
|
||||
# flatpak (optional) | claude CLI (required for --AI)
|
||||
# State: /updatestate — ISO timestamp of last completed update
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CONSTANTS & FLAGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
readonly STATE_FILE="/updatestate"
|
||||
readonly SCRIPT_PREFIX="/updatescript"
|
||||
readonly NEWS_FEED="https://archlinux.org/feeds/news/"
|
||||
|
||||
AI_MODE=false
|
||||
for _arg in "$@"; do [[ "$_arg" == "--AI" ]] && AI_MODE=true; done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# TERMINAL / COLOR SETUP
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[1;33m'
|
||||
B=$'\033[0;34m' C=$'\033[0;36m' M=$'\033[0;35m'
|
||||
BO=$'\033[1m' DI=$'\033[2m' RS=$'\033[0m'
|
||||
else
|
||||
R='' G='' Y='' B='' C='' M='' BO='' DI='' RS=''
|
||||
fi
|
||||
|
||||
_cols() { tput cols 2>/dev/null || echo 80; }
|
||||
|
||||
hline() {
|
||||
local ch="${1:-─}" col="${2:-$DI}"
|
||||
local n; n=$(_cols)
|
||||
printf '%s' "$col"
|
||||
printf '%*s' "$n" '' | tr ' ' "$ch"
|
||||
printf '%s\n' "$RS"
|
||||
}
|
||||
|
||||
header() {
|
||||
tput clear 2>/dev/null || clear
|
||||
hline "═" "$C"
|
||||
local ai_tag=""; [[ "$AI_MODE" == true ]] && ai_tag=" ${M}[AI MODE]${C}"
|
||||
printf " %s%s Arch Linux System Updater%s%s\n" "$C$BO" "⟨ SYSUPDATE ⟩" "$ai_tag" "$RS"
|
||||
hline "═" "$C"
|
||||
echo
|
||||
}
|
||||
|
||||
section() {
|
||||
echo
|
||||
local title=" ┌─ $* "
|
||||
local pad=$(( $(_cols) - ${#title} - 1 ))
|
||||
printf '%s%s%s' "$BO$Y" "$title"
|
||||
[[ $pad -gt 0 ]] && printf '─%.0s' $(seq 1 $pad)
|
||||
printf '┐%s\n' "$RS"
|
||||
}
|
||||
|
||||
log() { printf " ${B}▸${RS} %s\n" "$*"; }
|
||||
ok() { printf " ${G}✓${RS} %s\n" "$*"; }
|
||||
warn() { printf " ${Y}⚠${RS} %s\n" "$*"; }
|
||||
err() { printf " ${R}✗${RS} %s\n" "$*" >&2; }
|
||||
step() { printf "\n ${C}${BO}[%d/%d]${RS} %s\n" "$1" "$2" "$3"; }
|
||||
|
||||
ask() {
|
||||
printf " ${Y}?${RS} ${BO}%s${RS} ${DI}[Y/n]${RS} " "$*"
|
||||
local r; read -r r
|
||||
[[ "${r,,}" != "n" ]]
|
||||
}
|
||||
|
||||
_spin_pid=""
|
||||
spin_start() {
|
||||
local msg="$*"
|
||||
( local f=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') i=0
|
||||
while true; do
|
||||
printf "\r ${C}%s${RS} %s " "${f[$i]}" "$msg"
|
||||
(( i = (i+1) % 10 )); sleep 0.08
|
||||
done ) &
|
||||
_spin_pid=$!
|
||||
disown "$_spin_pid" 2>/dev/null
|
||||
}
|
||||
|
||||
spin_stop() {
|
||||
[[ -n "${_spin_pid:-}" ]] && { kill "$_spin_pid" 2>/dev/null; wait "$_spin_pid" 2>/dev/null || true; _spin_pid=""; }
|
||||
printf '\r%*s\r' "$(_cols)" ''
|
||||
}
|
||||
|
||||
trap 'spin_stop; tput cnorm 2>/dev/null || true' EXIT
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# STATE FILE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
read_state() {
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
cat "$STATE_FILE"
|
||||
return
|
||||
fi
|
||||
|
||||
# No state file — derive baseline from the running kernel's install date.
|
||||
local uname_r; uname_r=$(uname -r)
|
||||
local -a kpkgs=()
|
||||
|
||||
# 1. Discover kernel packages via /boot/vmlinuz ownership (covers all flavors)
|
||||
for vmlinuz in /boot/vmlinuz-linux /boot/vmlinuz-linux-*; do
|
||||
[[ -f "$vmlinuz" ]] || continue
|
||||
local qo; qo=$(pacman -Qo "$vmlinuz" 2>/dev/null | awk '{print $NF}')
|
||||
[[ -n "$qo" ]] && kpkgs+=("$qo")
|
||||
done
|
||||
|
||||
# 2. Fallback: scan the pacman DB for installed linux kernel packages
|
||||
if [[ ${#kpkgs[@]} -eq 0 ]]; then
|
||||
mapfile -t kpkgs < <(
|
||||
pacman -Qq 2>/dev/null \
|
||||
| grep -E '^linux(-[a-z0-9]+(-[a-z0-9]+)*)?$' \
|
||||
| grep -vE '(-headers|-docs|-firmware|-api|-utils|-tools)$' \
|
||||
|| true
|
||||
)
|
||||
fi
|
||||
|
||||
# Deduplicate
|
||||
mapfile -t kpkgs < <(printf '%s\n' "${kpkgs[@]:-}" | sort -u)
|
||||
|
||||
if [[ ${#kpkgs[@]} -eq 0 ]]; then
|
||||
# No kernel package found at all — hard fallback
|
||||
date -d "30 days ago" "+%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "1970-01-01T00:00:00"
|
||||
return
|
||||
fi
|
||||
|
||||
# 3. Match the running kernel's flavor to a package.
|
||||
# uname -r examples:
|
||||
# 6.14.5-arch1-1 → linux
|
||||
# 6.6.88-1-lts → linux-lts
|
||||
# 6.14.4-zen1-1-zen → linux-zen
|
||||
# 6.14.4-hardened1-1-... → linux-hardened
|
||||
# 6.14.5-cachyos-1 → linux-cachyos / linux-cachyos-*
|
||||
local best_pkg="" best_epoch=0 found_running=false
|
||||
|
||||
for pkg in "${kpkgs[@]}"; do
|
||||
# Extract flavor suffix: linux → "", linux-lts → "lts", linux-zen → "zen", etc.
|
||||
local suffix="${pkg#linux}"; suffix="${suffix#-}"
|
||||
|
||||
local is_running=false
|
||||
if [[ -z "$suffix" ]]; then
|
||||
# Mainline: uname -r looks like N.N.N-arch<n>-<n>
|
||||
[[ "$uname_r" =~ ^[0-9]+\.[0-9]+\.[0-9]+-arch ]] && is_running=true
|
||||
else
|
||||
# Any other flavor: check that the suffix appears in uname -r
|
||||
[[ "${uname_r,,}" == *"${suffix,,}"* ]] && is_running=true
|
||||
fi
|
||||
|
||||
local install_date; install_date=$(
|
||||
pacman -Qi "$pkg" 2>/dev/null \
|
||||
| awk -F': ' '/^Install Date/{sub(/^ +/,"",$2); print $2; exit}'
|
||||
)
|
||||
[[ -z "$install_date" ]] && continue
|
||||
|
||||
local epoch; epoch=$(date -d "$install_date" +%s 2>/dev/null || echo 0)
|
||||
|
||||
if $is_running; then
|
||||
best_pkg="$pkg"; best_epoch=$epoch; found_running=true; break
|
||||
fi
|
||||
# Keep track of most recently installed as fallback
|
||||
if [[ $epoch -gt $best_epoch ]]; then
|
||||
best_pkg="$pkg"; best_epoch=$epoch
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$best_pkg" ]]; then
|
||||
date -d "30 days ago" "+%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "1970-01-01T00:00:00"
|
||||
return
|
||||
fi
|
||||
|
||||
local install_date; install_date=$(
|
||||
pacman -Qi "$best_pkg" 2>/dev/null \
|
||||
| awk -F': ' '/^Install Date/{sub(/^ +/,"",$2); print $2; exit}'
|
||||
)
|
||||
local iso; iso=$(date -d "$install_date" "+%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$iso" ]]; then
|
||||
local label; $found_running && label="running kernel" || label="most recently installed kernel"
|
||||
warn "No state file — using ${BO}${best_pkg}${RS} ($label) install date as baseline: $iso"
|
||||
echo "$iso"
|
||||
else
|
||||
date -d "30 days ago" "+%Y-%m-%dT%H:%M:%S" 2>/dev/null || echo "1970-01-01T00:00:00"
|
||||
fi
|
||||
}
|
||||
|
||||
write_state() {
|
||||
local ts; ts=$(date "+%Y-%m-%dT%H:%M:%S")
|
||||
echo "$ts" | sudo tee "$STATE_FILE" > /dev/null
|
||||
sudo chmod 644 "$STATE_FILE" 2>/dev/null || true
|
||||
ok "State file updated → $STATE_FILE ($ts)"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PACKAGE COLLECTION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
get_pacman_updates() { checkupdates 2>/dev/null | awk '{print $1}' || true; }
|
||||
get_aur_updates() { yay -Qua 2>/dev/null | awk '{print $1}' || true; }
|
||||
get_flatpak_updates(){ flatpak remote-ls --updates 2>/dev/null | awk 'NR>0{print $1}' || true; }
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# ARCH NEWS — Python helper written to a temp file once at startup
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
_NEWS_PY=$(mktemp /tmp/sysupdate-news.XXXXXX.py)
|
||||
|
||||
cat > "$_NEWS_PY" << 'PYEOF'
|
||||
import sys, xml.etree.ElementTree as ET, email.utils, re, html, datetime, time
|
||||
|
||||
since_str = sys.argv[1] if len(sys.argv) > 1 else "1970-01-01T00:00:00"
|
||||
try:
|
||||
since_epoch = int(datetime.datetime.fromisoformat(since_str).timestamp())
|
||||
except Exception:
|
||||
since_epoch = 0
|
||||
|
||||
data = sys.stdin.read()
|
||||
if not data.strip():
|
||||
sys.exit(0)
|
||||
try:
|
||||
root = ET.fromstring(data)
|
||||
except ET.ParseError:
|
||||
sys.exit(0)
|
||||
|
||||
for item in root.findall('.//item'):
|
||||
title = (item.findtext('title') or '').strip()
|
||||
pubdate = (item.findtext('pubDate') or '').strip()
|
||||
desc = (item.findtext('description') or '').strip()
|
||||
link = (item.findtext('link') or '').strip()
|
||||
|
||||
try:
|
||||
epoch = int(email.utils.mktime_tz(email.utils.parsedate_tz(pubdate)))
|
||||
except Exception:
|
||||
epoch = 0
|
||||
|
||||
if epoch < since_epoch:
|
||||
continue
|
||||
|
||||
desc_clean = html.unescape(re.sub(r'<[^>]+>', ' ', desc))
|
||||
desc_clean = re.sub(r'\s+', ' ', desc_clean).strip()[:1000]
|
||||
try:
|
||||
human = time.strftime('%Y-%m-%d', time.localtime(epoch))
|
||||
except Exception:
|
||||
human = pubdate
|
||||
|
||||
print(f"TITLE:{title}")
|
||||
print(f"DATE:{human}")
|
||||
print(f"LINK:{link}")
|
||||
print(f"DESC:{desc_clean}")
|
||||
print("---")
|
||||
PYEOF
|
||||
|
||||
fetch_news() {
|
||||
local since="$1"
|
||||
curl -s --connect-timeout 10 --max-time 20 "$NEWS_FEED" 2>/dev/null \
|
||||
| python3 "$_NEWS_PY" "$since" || true
|
||||
}
|
||||
|
||||
# Fills global associative array PKG_NEWS[pkg]="formatted news text"
|
||||
declare -A PKG_NEWS=()
|
||||
|
||||
parse_and_match_news() {
|
||||
local news_raw="$1"; shift
|
||||
local -a pkgs=("$@")
|
||||
[[ -z "$news_raw" || ${#pkgs[@]} -eq 0 ]] && return 0
|
||||
|
||||
local title="" date="" link="" desc=""
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" == TITLE:* ]]; then title="${line#TITLE:}"
|
||||
elif [[ "$line" == DATE:* ]]; then date="${line#DATE:}"
|
||||
elif [[ "$line" == LINK:* ]]; then link="${line#LINK:}"
|
||||
elif [[ "$line" == DESC:* ]]; then desc="${line#DESC:}"
|
||||
elif [[ "$line" == "---" ]]; then
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
[[ -z "$pkg" ]] && continue
|
||||
local base="$pkg"
|
||||
base="${base#lib32-}"; base="${base#python-}"; base="${base#perl-}"
|
||||
base="${base%-git}"; base="${base%-bin}"; base="${base%-dev}"
|
||||
local hay="${title,,} ${desc,,}"
|
||||
if [[ "$hay" == *"${pkg,,}"* ]] || \
|
||||
[[ ${#base} -gt 3 && "$hay" == *"${base,,}"* ]]; then
|
||||
PKG_NEWS["$pkg"]+="$(printf '[%s] %s\n%s\n%s\n\n' \
|
||||
"$date" "$title" "$desc" "$link")"
|
||||
fi
|
||||
done
|
||||
title="" date="" link="" desc=""
|
||||
fi
|
||||
done <<< "$news_raw"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PACKAGE SELECTOR — dialog checklist with fallback
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
declare -a SELECTED_PACMAN=()
|
||||
declare -a SELECTED_AUR=()
|
||||
|
||||
select_packages() {
|
||||
local -a p_pkgs=("${PACMAN_PKGS[@]:-}")
|
||||
local -a a_pkgs=("${AUR_PKGS[@]:-}")
|
||||
|
||||
if ! command -v dialog &>/dev/null; then
|
||||
warn "'dialog' not installed — all packages will be updated"
|
||||
SELECTED_PACMAN=("${p_pkgs[@]:-}")
|
||||
SELECTED_AUR=("${a_pkgs[@]:-}")
|
||||
return
|
||||
fi
|
||||
|
||||
local items=()
|
||||
for pkg in "${p_pkgs[@]:-}"; do
|
||||
[[ -n "$pkg" ]] && items+=("p:$pkg" "(pacman)" "on")
|
||||
done
|
||||
for pkg in "${a_pkgs[@]:-}"; do
|
||||
[[ -n "$pkg" ]] && items+=("a:$pkg" "(AUR)" "on")
|
||||
done
|
||||
[[ ${#items[@]} -eq 0 ]] && return
|
||||
|
||||
local chosen
|
||||
chosen=$(dialog --stdout --separate-output \
|
||||
--title " Package Selection " \
|
||||
--checklist " SPACE = toggle | ENTER = confirm | ESC = abort " \
|
||||
0 60 0 "${items[@]}") || { warn "Cancelled."; exit 0; }
|
||||
|
||||
SELECTED_PACMAN=(); SELECTED_AUR=()
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
case "$line" in
|
||||
p:*) SELECTED_PACMAN+=("${line#p:}") ;;
|
||||
a:*) SELECTED_AUR+=("${line#a:}") ;;
|
||||
esac
|
||||
done <<< "$chosen"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# NEWS REVIEW — interactive per-package review (non-AI mode)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
NEWS_DECISION="" # ok | skip | shell | quit
|
||||
|
||||
show_pkg_news() {
|
||||
local pkg="$1" news_text="$2"
|
||||
local w; w=$(_cols)
|
||||
while true; do
|
||||
tput clear 2>/dev/null || clear
|
||||
hline "═" "$C"
|
||||
printf " ${BO}NEWS FOR: ${Y}%s${RS}\n" "$pkg"
|
||||
hline "═" "$C"
|
||||
echo
|
||||
echo "$news_text" | fold -s -w $(( w - 4 )) | sed 's/^/ /'
|
||||
echo
|
||||
hline "─" "$DI"
|
||||
printf " ${BO}[c]${RS} Continue ${BO}[s]${RS} Skip package ${BO}[b]${RS} Open shell ${BO}[q]${RS} Quit all\n"
|
||||
hline "─" "$DI"
|
||||
printf " → "
|
||||
local key; read -rn1 key; echo
|
||||
case "${key,,}" in
|
||||
c|"") NEWS_DECISION="ok"; return ;;
|
||||
s) NEWS_DECISION="skip"; return ;;
|
||||
b) NEWS_DECISION="shell"; return ;;
|
||||
q) NEWS_DECISION="quit"; return ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# ONE-BY-ONE UPDATERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
update_pacman_onebyone() {
|
||||
local -a pkgs=("$@")
|
||||
local total=${#pkgs[@]} n=0
|
||||
local -a failed=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
(( n++ ))
|
||||
step "$n" "$total" "pacman: ${BO}$pkg${RS}"
|
||||
if sudo pacman -S --noconfirm --needed "$pkg"; then
|
||||
ok "$pkg"
|
||||
else
|
||||
err "$pkg FAILED"
|
||||
failed+=("$pkg")
|
||||
fi
|
||||
done
|
||||
[[ ${#failed[@]} -gt 0 ]] && warn "Pacman failures: ${failed[*]}"
|
||||
return ${#failed[@]}
|
||||
}
|
||||
|
||||
update_aur_onebyone() {
|
||||
local -a pkgs=("$@")
|
||||
local total=${#pkgs[@]} n=0
|
||||
local -a failed=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
(( n++ ))
|
||||
step "$n" "$total" "yay: ${BO}$pkg${RS}"
|
||||
if yay -S --noconfirm --answerdiff None --answerclean All --removemake "$pkg"; then
|
||||
ok "$pkg"
|
||||
else
|
||||
err "$pkg FAILED"
|
||||
failed+=("$pkg")
|
||||
fi
|
||||
done
|
||||
[[ ${#failed[@]} -gt 0 ]] && warn "AUR failures: ${failed[*]}"
|
||||
return ${#failed[@]}
|
||||
}
|
||||
|
||||
update_flatpak() {
|
||||
log "Updating flatpaks..."
|
||||
if flatpak update -y; then
|
||||
ok "Flatpak update complete"
|
||||
else
|
||||
warn "Flatpak update failed"
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# AI MODE — generate timestamped update script via Claude
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ai_generate_script() {
|
||||
local ts; ts=$(date "+%Y%m%d-%H%M%S")
|
||||
local outfile="${SCRIPT_PREFIX}-${ts}.sh"
|
||||
|
||||
local pkg_list=""
|
||||
for p in "${SELECTED_PACMAN[@]:-}"; do [[ -n "$p" ]] && pkg_list+=" [pacman] $p\n"; done
|
||||
for p in "${SELECTED_AUR[@]:-}"; do [[ -n "$p" ]] && pkg_list+=" [aur] $p\n"; done
|
||||
for p in "${FLATPAK_PKGS[@]:-}"; do [[ -n "$p" ]] && pkg_list+=" [flatpak] $p\n"; done
|
||||
|
||||
local news_section=""
|
||||
for pkg in "${!PKG_NEWS[@]}"; do
|
||||
news_section+="=== $pkg ===\n${PKG_NEWS[$pkg]}\n"
|
||||
done
|
||||
[[ -z "$news_section" ]] && news_section="(no relevant news items found for this update batch)"
|
||||
|
||||
read -r -d '' prompt << PROMPT_EOF || true
|
||||
You are an Arch Linux sysadmin generating a safe one-by-one system update script.
|
||||
|
||||
TODAY: $(date)
|
||||
LAST UPDATE: $(read_state)
|
||||
OUTPUT FILE: $outfile
|
||||
|
||||
PACKAGES TO UPDATE:
|
||||
$(printf '%b' "$pkg_list")
|
||||
|
||||
ARCH LINUX NEWS SINCE LAST UPDATE:
|
||||
$(printf '%b' "$news_section")
|
||||
|
||||
RULES:
|
||||
- Output ONLY valid bash starting with #!/usr/bin/env bash — zero prose.
|
||||
- Track failures in a failed=() array; report at the end.
|
||||
- Update order: flatpak first, then pacman packages, then AUR packages.
|
||||
- Pacman one-by-one: sudo pacman -S --noconfirm --needed <pkg>
|
||||
- AUR one-by-one: yay -S --noconfirm --answerdiff None --answerclean All --removemake <pkg>
|
||||
- Flatpak: flatpak update -y
|
||||
- If news mentions required manual steps for a package, insert them immediately before/after that package with a brief comment explaining WHY.
|
||||
- On overall success, write the current ISO timestamp to ${STATE_FILE}:
|
||||
echo "\$(date +%Y-%m-%dT%H:%M:%S)" | sudo tee "${STATE_FILE}" >/dev/null
|
||||
PROMPT_EOF
|
||||
|
||||
section "AI Analysis"
|
||||
spin_start "Claude is analysing news and generating update script..."
|
||||
|
||||
local result
|
||||
if result=$(claude -p "$prompt" 2>/dev/null); then
|
||||
spin_stop
|
||||
# Strip markdown fences if Claude added them
|
||||
result=$(printf '%s\n' "$result" | sed '/^```/d')
|
||||
[[ "$result" != '#!/'* ]] && result="#!/usr/bin/env bash"$'\n'"$result"
|
||||
echo "$result" | sudo tee "$outfile" > /dev/null
|
||||
sudo chmod 755 "$outfile"
|
||||
ok "Script saved → ${BO}$outfile${RS}"
|
||||
echo
|
||||
printf " ${DI}─── Preview (first 35 lines) ───${RS}\n\n"
|
||||
head -35 "$outfile" | sed 's/^/ /'
|
||||
echo
|
||||
printf " ${DI}───────────────────────────────${RS}\n"
|
||||
echo "$outfile"
|
||||
else
|
||||
spin_stop
|
||||
err "Claude CLI unavailable or returned an error."
|
||||
err "Make sure 'claude' is in PATH and authenticated."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# MAIN
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
declare -a PACMAN_PKGS=()
|
||||
declare -a AUR_PKGS=()
|
||||
declare -a FLATPAK_PKGS=()
|
||||
|
||||
main() {
|
||||
header
|
||||
|
||||
# ── Read state ───────────────────────────────────────────────────────────
|
||||
local last_update; last_update=$(read_state)
|
||||
log "Last recorded update: ${BO}${last_update}${RS}"
|
||||
echo
|
||||
|
||||
# ── Collect available updates ────────────────────────────────────────────
|
||||
section "Collecting Updates"
|
||||
|
||||
spin_start "Checking pacman packages (checkupdates)..."
|
||||
mapfile -t PACMAN_PKGS < <(get_pacman_updates)
|
||||
spin_stop; log "Pacman: ${BO}${#PACMAN_PKGS[@]}${RS} package(s)"
|
||||
|
||||
spin_start "Checking AUR packages (yay -Qua)..."
|
||||
mapfile -t AUR_PKGS < <(get_aur_updates)
|
||||
spin_stop; log "AUR: ${BO}${#AUR_PKGS[@]}${RS} package(s)"
|
||||
|
||||
if command -v flatpak &>/dev/null; then
|
||||
spin_start "Checking flatpak packages..."
|
||||
mapfile -t FLATPAK_PKGS < <(get_flatpak_updates)
|
||||
spin_stop; log "Flatpak: ${BO}${#FLATPAK_PKGS[@]}${RS} package(s)"
|
||||
else
|
||||
log "Flatpak: (not installed)"
|
||||
fi
|
||||
|
||||
local total=$(( ${#PACMAN_PKGS[@]} + ${#AUR_PKGS[@]} + ${#FLATPAK_PKGS[@]} ))
|
||||
|
||||
if [[ $total -eq 0 ]]; then
|
||||
echo
|
||||
ok "${BO}System is up to date.${RS}"
|
||||
write_state
|
||||
rm -f "$_NEWS_PY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo; ok "${BO}$total${RS} package(s) available"
|
||||
|
||||
# ── Package selection via dialog checklist ───────────────────────────────
|
||||
section "Package Selection"
|
||||
select_packages
|
||||
|
||||
local total_sel=$(( ${#SELECTED_PACMAN[@]} + ${#SELECTED_AUR[@]} + ${#FLATPAK_PKGS[@]} ))
|
||||
if [[ $total_sel -eq 0 ]]; then
|
||||
warn "No packages selected. Exiting."; exit 0
|
||||
fi
|
||||
log "Will update: ${#SELECTED_PACMAN[@]} pacman | ${#SELECTED_AUR[@]} AUR | ${#FLATPAK_PKGS[@]} flatpak"
|
||||
|
||||
# ── Fetch and match Arch Linux news ─────────────────────────────────────
|
||||
section "Arch Linux News"
|
||||
|
||||
local all_sel=("${SELECTED_PACMAN[@]:-}" "${SELECTED_AUR[@]:-}")
|
||||
local news_raw=""
|
||||
|
||||
spin_start "Fetching $NEWS_FEED ..."
|
||||
news_raw=$(fetch_news "$last_update")
|
||||
spin_stop
|
||||
|
||||
if [[ -z "$news_raw" ]]; then
|
||||
warn "No news fetched (no items in range, or network unavailable)"
|
||||
else
|
||||
parse_and_match_news "$news_raw" "${all_sel[@]:-}"
|
||||
if [[ ${#PKG_NEWS[@]} -gt 0 ]]; then
|
||||
warn "${BO}${#PKG_NEWS[@]}${RS} package(s) have relevant news: ${!PKG_NEWS[*]}"
|
||||
else
|
||||
ok "No news matched for the selected packages"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── AI mode: generate script ─────────────────────────────────────────────
|
||||
if [[ "$AI_MODE" == true ]]; then
|
||||
local script_path
|
||||
script_path=$(ai_generate_script)
|
||||
echo
|
||||
printf " ${G}${BO}Script:${RS} ${BO}%s${RS}\n" "$script_path"
|
||||
printf " Run it with: ${C}sudo bash %s${RS}\n\n" "$script_path"
|
||||
if ask "Run the generated script now?"; then
|
||||
echo
|
||||
sudo bash "$script_path"
|
||||
fi
|
||||
rm -f "$_NEWS_PY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Non-AI mode: per-package news review ────────────────────────────────
|
||||
if [[ ${#PKG_NEWS[@]} -gt 0 ]]; then
|
||||
section "News Review"
|
||||
log "Review news for each affected package before proceeding."
|
||||
echo
|
||||
|
||||
local -a skip_pkgs=()
|
||||
for pkg in "${!PKG_NEWS[@]}"; do
|
||||
show_pkg_news "$pkg" "${PKG_NEWS[$pkg]}"
|
||||
case "$NEWS_DECISION" in
|
||||
skip)
|
||||
skip_pkgs+=("$pkg")
|
||||
ok "Will skip: $pkg"
|
||||
;;
|
||||
shell)
|
||||
printf "\n ${Y}Opening adjustment shell — type 'exit' to return.${RS}\n\n"
|
||||
PS1="${Y}[sysupdate-shell \w]\$${RS} " bash --norc --noprofile
|
||||
# Ask again after returning from shell
|
||||
show_pkg_news "$pkg" "${PKG_NEWS[$pkg]}"
|
||||
[[ "$NEWS_DECISION" == "skip" ]] && skip_pkgs+=("$pkg") && ok "Will skip: $pkg"
|
||||
[[ "$NEWS_DECISION" == "quit" ]] && { warn "Aborted."; exit 0; }
|
||||
;;
|
||||
quit)
|
||||
warn "Aborted by user."; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Remove skipped packages from selection
|
||||
if [[ ${#skip_pkgs[@]} -gt 0 ]]; then
|
||||
local -a fp=() fa=()
|
||||
for pkg in "${SELECTED_PACMAN[@]:-}"; do
|
||||
printf '%s\n' "${skip_pkgs[@]}" | grep -qxF "$pkg" || fp+=("$pkg")
|
||||
done
|
||||
for pkg in "${SELECTED_AUR[@]:-}"; do
|
||||
printf '%s\n' "${skip_pkgs[@]}" | grep -qxF "$pkg" || fa+=("$pkg")
|
||||
done
|
||||
SELECTED_PACMAN=("${fp[@]:-}"); SELECTED_AUR=("${fa[@]:-}")
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Final confirmation ───────────────────────────────────────────────────
|
||||
section "Ready to Update"
|
||||
echo
|
||||
|
||||
if [[ ${#SELECTED_PACMAN[@]} -gt 0 ]]; then
|
||||
printf " ${BO}pacman (%d):${RS}\n" "${#SELECTED_PACMAN[@]}"
|
||||
printf ' %s• %s%s\n' "$M" "$RS" "${SELECTED_PACMAN[@]}"
|
||||
echo
|
||||
fi
|
||||
if [[ ${#SELECTED_AUR[@]} -gt 0 ]]; then
|
||||
printf " ${BO}AUR (%d):${RS}\n" "${#SELECTED_AUR[@]}"
|
||||
printf ' %s• %s%s\n' "$M" "$RS" "${SELECTED_AUR[@]}"
|
||||
echo
|
||||
fi
|
||||
if [[ ${#FLATPAK_PKGS[@]} -gt 0 ]]; then
|
||||
printf " ${BO}flatpak (%d):${RS}\n" "${#FLATPAK_PKGS[@]}"
|
||||
printf ' %s• %s%s\n' "$M" "$RS" "${FLATPAK_PKGS[@]}"
|
||||
echo
|
||||
fi
|
||||
|
||||
ask "Proceed with update?" || { warn "Aborted."; exit 0; }
|
||||
|
||||
# ── Execute updates ──────────────────────────────────────────────────────
|
||||
section "Updating System"
|
||||
|
||||
local exit_code=0
|
||||
|
||||
if [[ ${#FLATPAK_PKGS[@]} -gt 0 ]]; then
|
||||
echo
|
||||
printf " ${BO}── Flatpak ──${RS}\n"
|
||||
update_flatpak || (( exit_code++ )) || true
|
||||
fi
|
||||
|
||||
if [[ ${#SELECTED_PACMAN[@]} -gt 0 ]]; then
|
||||
echo
|
||||
printf " ${BO}── Pacman ──${RS}\n"
|
||||
update_pacman_onebyone "${SELECTED_PACMAN[@]}" || (( exit_code++ )) || true
|
||||
fi
|
||||
|
||||
if [[ ${#SELECTED_AUR[@]} -gt 0 ]]; then
|
||||
echo
|
||||
printf " ${BO}── AUR ──${RS}\n"
|
||||
update_aur_onebyone "${SELECTED_AUR[@]}" || (( exit_code++ )) || true
|
||||
fi
|
||||
|
||||
# ── Write state and finish ───────────────────────────────────────────────
|
||||
echo
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
write_state
|
||||
hline "═" "$G"
|
||||
printf " ${G}${BO}✓ Update complete!${RS}\n"
|
||||
hline "═" "$G"
|
||||
else
|
||||
warn "Some packages failed — state file NOT updated."
|
||||
hline "═" "$Y"
|
||||
printf " ${Y}${BO}⚠ Update finished with errors.${RS}\n"
|
||||
hline "═" "$Y"
|
||||
fi
|
||||
echo
|
||||
|
||||
rm -f "$_NEWS_PY"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Reference in New Issue