From fbe9134c2250ec51517796b44e97af497c61bedf Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 21 Aug 2026 16:19:26 +0200 Subject: [PATCH] pacman_wrapper: split into flat libs under the 250-line cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 929 -> 217 lines across five pw_*.sh libraries. This file IS /usr/bin/pacman here (the symlink points at the deployed copy), so the design constraint was not readability but that no failure mode may leave the machine unable to install packages. Fail OPEN, unlike the other splits in this campaign. A missing lib warns on stderr and execs real pacman unwrapped. Failing closed would mean a missing lib leaves no way to install anything, including the missing lib. This is the same choice the existing pacman_lock_lib.sh block already makes, and it is verified rather than asserted: with pw_guard.sh removed from a scratch-dir copy, the wrapper warns, execs, and returns real `-Qi bash` output at rc=0. The libs load at the very top, before the fast path, because the fast path calls needs_unlock() to decide whether to exec straight through, and that function now lives in pw_guard.sh. They are deployed flat beside the wrapper — the shape pacman_lock_lib.sh and heavy_job_lock.sh already use in /usr/local/bin — as required copies, and are added to the integrity manifest. Tamper detection was round-tripped in a fake root: 8 manifest entries verify clean, and appending one line to pw_challenge.sh is detected and the transaction denied. Verified by execution, not just textually: real `-Qi bash` and `-Q` through the split wrapper are byte-identical to the pre-split baseline, same 2824 packages. verify_shell_split.sh reports all 14 top-level functions moved verbatim. The deployed copy is deliberately NOT updated by this commit and still hashes 548fc2b5... exactly as before the work started; live pacman is unaffected. A live `pacman -S` remains unverified — the Bash tool deadlocks on db.lck and a failed experiment costs the package manager. Cap is now down to one file. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV --- .../pacman-wrapper-split-2026-08-21.json | 19 + .../pacman-wrapper-split-2026-08-21.json | 65 ++ .../pacman/install_pacman_wrapper.sh | 4 + .../pacman/lib/install_files.sh | 12 + .../digital_wellbeing/pacman/lib/integrity.sh | 12 + .../pacman/pacman_wrapper.sh | 788 +----------------- .../digital_wellbeing/pacman/pw_challenge.sh | 177 ++++ .../digital_wellbeing/pacman/pw_cleanup.sh | 153 ++++ .../digital_wellbeing/pacman/pw_guard.sh | 145 ++++ .../digital_wellbeing/pacman/pw_policy.sh | 198 +++++ .../digital_wellbeing/pacman/pw_ui.sh | 90 ++ 11 files changed, 912 insertions(+), 751 deletions(-) create mode 100644 docs/superpowers/contracts/pacman-wrapper-split-2026-08-21.json create mode 100644 docs/superpowers/evidence/pacman-wrapper-split-2026-08-21.json create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_challenge.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_cleanup.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_guard.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_policy.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_ui.sh diff --git a/docs/superpowers/contracts/pacman-wrapper-split-2026-08-21.json b/docs/superpowers/contracts/pacman-wrapper-split-2026-08-21.json new file mode 100644 index 00000000..f117ef1f --- /dev/null +++ b/docs/superpowers/contracts/pacman-wrapper-split-2026-08-21.json @@ -0,0 +1,19 @@ +{ + "title": "Split pacman_wrapper.sh under the 250-line cap", + "objective": "Bring pacman_wrapper.sh (929 lines) under the cap by moving its 31 functions into five flat-deployed libraries, without any failure mode that could leave this machine without a working package manager — /usr/bin/pacman is a symlink to the deployed copy.", + "acceptance_criteria": [ + "The entry script and all five pw_*.sh libs are at most 250 lines.", + "Real read-only pacman queries through the split wrapper (-Qi bash, -Q) are byte-identical to the pre-split baseline.", + "A missing library FAILS OPEN: warning on stderr, exec of real pacman, correct output and exit status.", + "The five libs are deployed by install_files.sh and covered by the integrity manifest; tampering with one is detected and denied.", + "verify_shell_split.sh reports all functions moved verbatim.", + "The deployed /usr/local/bin/pacman_wrapper is byte-identical to the pre-work backup, and live pacman still works.", + "shellcheck passes without -x, shfmt is a no-op, no suppression added." + ], + "out_of_scope": [ + "Running a live `pacman -S` transaction: the Bash tool deadlocks on db.lck and a failure costs the package manager.", + "Re-deploying to /usr/local/bin — a separate, user-run step.", + "The pre-existing SC2329 suppression on load_policy_lists, which predates this split." + ], + "verifier": "Real -Qi/-Q diffs against the captured baseline; a scratch-dir fail-open simulation with a lib removed; an integrity round-trip proving tamper detection covers the new libs; verify_shell_split.sh; sha256 of the deployed copy vs the pre-work backup; shellcheck + shfmt + check_file_length --all." +} diff --git a/docs/superpowers/evidence/pacman-wrapper-split-2026-08-21.json b/docs/superpowers/evidence/pacman-wrapper-split-2026-08-21.json new file mode 100644 index 00000000..f1e67013 --- /dev/null +++ b/docs/superpowers/evidence/pacman-wrapper-split-2026-08-21.json @@ -0,0 +1,65 @@ +{ + "intent": "Bring pacman_wrapper.sh (929 lines) under the 250-line cap. This file IS /usr/bin/pacman on this machine — /usr/bin/pacman is a symlink to the deployed copy — so the overriding constraint is that no failure mode may leave the box without a working package manager.", + "scope": [ + "pacman_wrapper.sh: 929 -> 217 lines", + "New libs, FLAT beside the wrapper: pw_policy.sh (188), pw_challenge.sh (177), pw_cleanup.sh (153), pw_guard.sh (145), pw_ui.sh (90)", + "install_pacman_wrapper.sh + lib/install_files.sh: deploy the five libs", + "lib/integrity.sh: checksum them in the policy manifest", + "Non-goal: redeploying to /usr/local/bin, and any live `pacman -S` transaction" + ], + "changes": [ + "All 31 functions moved into five libs by responsibility. This needed a fix to extract_shell_functions.py first (committed separately): 17 of the 31 use bash's `function name() {` spelling, which the extractor did not recognise and would have silently left behind.", + "Libs are deployed FLAT beside the wrapper, matching pacman_lock_lib.sh and heavy_job_lock.sh, which already live and are sourced that way in /usr/local/bin. No lib/ subdirectory is created there.", + "The source block sits at the TOP of the file, before the fast path, because the fast path calls needs_unlock() to decide whether to exec real pacman and that function now lives in pw_guard.sh.", + "FAIL OPEN, unlike every other split in this campaign. A missing lib prints a warning to stderr and execs real pacman unwrapped. Failing closed here would mean a missing lib leaves no way to install anything — including the missing lib. This mirrors the choice the pre-existing pacman_lock_lib.sh block already makes.", + "The five libs are registered in install_files.sh as REQUIRED copies and added to the integrity manifest. They are sourced before verify_policy_integrity runs (the fast path forces that ordering), so the manifest entry is what makes tampering detectable — the check still refuses the transaction afterwards.", + "BOLD and MAKEPKG_CAPPED_BIN are exported: they are read by pw_ui.sh and pw_cleanup.sh, which shellcheck cannot see across the source boundary without -x." + ], + "verification": [ + { + "command": "PACMAN_BIN=/usr/bin/pacman.orig bash pacman_wrapper.sh -Qi bash / -Q", + "result": "pass", + "evidence": "REAL pacman queries through the split wrapper, byte-identical to the pre-split baseline: -Qi bash matches exactly, and -Q lists the same 2824 packages. This is execution, not textual proof — better than the --help-only baseline the prompt expected for this file." + }, + { + "command": "fail-open simulation: flat layout in a scratch dir with pw_guard.sh removed", + "result": "pass", + "evidence": "THE critical safety property. With needs_unlock's lib missing, the wrapper prints 'pacman wrapper libraries missing; running pacman unwrapped.' to stderr, execs real pacman, returns rc=0 and real -Qi output. A missing lib cannot brick package management." + }, + { + "command": "integrity round-trip in a fake root", + "result": "pass", + "evidence": "Built the manifest exactly as the patched installer does: 8 entries (2 policy lists + lock lib + 5 new libs). Untampered libs verify clean. Appending one line to pw_challenge.sh is DETECTED and the wrapper denies operation. The new libs are genuinely covered, not a hole in the check." + }, + { + "command": "bash meta/scripts/verify_shell_split.sh HEAD <5 libs>", + "result": "pass", + "evidence": "'IDENTICAL: all 14 top-level functions moved verbatim' — no body changed." + }, + { + "command": "python3 line-multiset check", + "result": "pass", + "evidence": "Two apparent losses, both explained: BOLD and MAKEPKG_CAPPED_BIN now carry an `export` prefix. Both confirmed present in the entry script; no code line was lost." + }, + { + "command": "sha256sum /usr/local/bin/pacman_wrapper vs the backup taken before any edit", + "result": "pass", + "evidence": "548fc2b53ee4cea6... on both. The deployed copy is untouched by this commit, which is why live pacman is unaffected. `pacman -Qi bash` through the real /usr/bin/pacman symlink still works, and the symlink is intact." + }, + { + "command": "shellcheck (no -x), shfmt idempotence, hyphenated-key canary, check_file_length --all", + "result": "pass", + "evidence": "shellcheck rc=0 with no new suppression; shfmt a no-op on all eight touched files; canary clean. The over-cap list is down to a single file." + } + ], + "risks": [ + "NOT VERIFIED against a live `pacman -S` transaction. The Bash tool deadlocks on db.lck for interactive pacman, and a failed experiment here costs the package manager. The evidence is instead: identical real read-only queries, a proven fail-open path, and a proven integrity round-trip. Running `sudo install_pacman_wrapper.sh` followed by a real install remains the one check only the user can perform.", + "The deployed copy still lacks the pw_*.sh libs until install_pacman_wrapper.sh is re-run. That ordering is safe in both directions: the deployed monolith is unchanged and self-contained, and if the new entry script were deployed without libs it would fail open to plain pacman rather than break.", + "The libs are sourced before verify_policy_integrity, so a tampered lib executes before the manifest is checked. Unavoidable given the fast path needs needs_unlock, and the same was already true of the wrapper file itself; the transaction is still refused." + ], + "rollback": [ + "git revert the commit; pacman_wrapper.sh returns to one 929-line file and the pw_*.sh libs disappear.", + "No live rollback needed — the deployed copy was never written.", + "If the split HAD been deployed: `sudo cp /tmp/.../restore/pacman_wrapper.deployed.bak /usr/local/bin/pacman_wrapper` restores the byte-identical monolith, and /usr/bin/pacman.orig (a real 157 KB ELF, verified running Pacman v7.1.0) is the escape hatch if even that fails." + ] +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh index 0f3115af..ed6b4054 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh @@ -31,6 +31,10 @@ NC='\033[0m' # No Color # linter accepts. `$0` is the entry script either way. WRAPPER_SOURCE="$(dirname "$0")/pacman_wrapper.sh" LOCK_LIB_SOURCE="$(dirname "$0")/pacman_lock_lib.sh" +# The wrapper's own phase libraries, deployed FLAT beside it (same shape as +# pacman_lock_lib.sh). Named explicitly rather than globbed so a stray pw_*.sh +# in a working tree cannot be deployed by accident. +PW_LIBS=(pw_policy pw_guard pw_ui pw_challenge pw_cleanup) WORDS_SOURCE="$(dirname "$0")/words.txt" BLOCKED_SOURCE="$(dirname "$0")/pacman_blocked_keywords.txt" WHITELIST_SOURCE="$(dirname "$0")/pacman_whitelist.txt" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/install_files.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/install_files.sh index a993cb27..aa529c73 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/install_files.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/install_files.sh @@ -10,6 +10,18 @@ install_managed_files() { copy_managed_file "$WRAPPER_SOURCE" "$WRAPPER_DEST" required "wrapper script" copy_managed_file "$LOCK_LIB_SOURCE" "$LOCK_LIB_DEST" required "stale-lock library" chmod 644 "$LOCK_LIB_DEST" + + # The wrapper's phase libraries. They land flat beside the wrapper because + # pacman_wrapper.sh resolves them from its own directory, which is + # /usr/local/bin once installed. Required, not optional: without them the + # wrapper still runs but falls open to unwrapped pacman, so a partial + # install would silently disable every policy check. + local pw_lib + for pw_lib in "${PW_LIBS[@]}"; do + copy_managed_file "$(dirname "$WRAPPER_SOURCE")/${pw_lib}.sh" \ + "${INSTALL_DIR}/${pw_lib}.sh" required "wrapper library ${pw_lib}" + chmod 644 "${INSTALL_DIR}/${pw_lib}.sh" + done copy_managed_file "$WORDS_SOURCE" "$WORDS_DEST" required "words list" copy_managed_file "$BLOCKED_SOURCE" "$BLOCKED_DEST" required "blocked keywords list" copy_managed_file "$WHITELIST_SOURCE" "$WHITELIST_DEST" optional "whitelist" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/integrity.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/integrity.sh index d14124f3..ba5dea16 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/integrity.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/lib/integrity.sh @@ -46,6 +46,18 @@ write_policy_integrity_file() { echo -e "${RED}Failed to checksum lock library${NC}" >&2 exit 1 } + # Same reasoning for the wrapper's phase libraries: they are sourced by + # pacman_wrapper.sh, so an unchecksummed lib would be an unguarded way + # to run code inside the wrapper. They are sourced BEFORE + # verify_policy_integrity (the fast path needs needs_unlock), so this + # manifest entry is what makes tampering detectable rather than what + # prevents the source — the check still refuses the transaction. + for pw_lib in "${PW_LIBS[@]}"; do + sha256sum "${INSTALL_DIR}/${pw_lib}.sh" || { + echo -e "${RED}Failed to checksum wrapper library ${pw_lib}${NC}" >&2 + exit 1 + } + done # Whitelist is optional if [[ -f "$WHITELIST_DEST" ]]; then sha256sum "$WHITELIST_DEST" || { diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh index 0f43f9ea..b5e25eb2 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh @@ -8,658 +8,57 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' -BOLD='\033[1m' +# Exported: consumed by pw_ui.sh and pw_cleanup.sh, which are sourced +# separately, so shellcheck cannot see the use without -x. +export BOLD='\033[1m' NC='\033[0m' # No Color PACMAN_BIN="/usr/bin/pacman" + +# --------------------------------------------------------------------------- +# Libraries. Sourced HERE, at the very top, because the fast path below execs +# real pacman and needs_unlock() — which decides whether to take it — now lives +# in pw_guard.sh. Deployed FLAT beside this script in /usr/local/bin, the same +# shape as pacman_lock_lib.sh and heavy_job_lock.sh; in the repo they sit +# beside it too, so one resolution rule covers both. +# +# FAIL OPEN, deliberately, and unlike every other split in this campaign: this +# file IS /usr/bin/pacman. If a lib goes missing, aborting would leave the +# machine with no working package manager and no way to reinstall the lib. So +# a missing lib degrades to plain pacman with a warning on stderr, which is +# the same choice the existing pacman_lock_lib.sh block below already makes. +# +# The integrity check that guards the POLICY files still runs below, before +# any transaction is allowed through. +# --------------------------------------------------------------------------- +_WRAPPER_DIR="$(dirname "$(readlink -f "$0")")" +for _pw_lib in pw_policy pw_guard pw_ui pw_challenge pw_cleanup; do + if [[ -r "$_WRAPPER_DIR/${_pw_lib}.sh" ]]; then + # shellcheck source=/dev/null + source "$_WRAPPER_DIR/${_pw_lib}.sh" + fi +done +unset _pw_lib + +if ! declare -F needs_unlock >/dev/null || ! declare -F verify_policy_integrity >/dev/null; then + printf 'Warning: pacman wrapper libraries missing; running pacman unwrapped.\n' >&2 + exec "$PACMAN_BIN" "$@" +fi # Installed alongside the wrapper by install_pacman_wrapper.sh. Absent on a # machine that has not run it yet, in which case transactions simply are not # serialised (see the guarded source below). HEAVY_JOB_LOCK_LIB="/usr/local/bin/heavy_job_lock.sh" -MAKEPKG_CAPPED_BIN="/usr/local/bin/makepkg_capped" +# Exported: used only by pw_ui.sh (sourced separately). +export MAKEPKG_CAPPED_BIN="/usr/local/bin/makepkg_capped" -declare -a BLOCKED_KEYWORDS_LIST=() -declare -a WHITELISTED_NAMES_LIST=() -declare -a GREYLISTED_KEYWORDS_LIST=() -POLICY_LISTS_LOADED=0 -INTEGRITY_DIR="/var/lib/pacman-wrapper" -INTEGRITY_FILE="${INTEGRITY_DIR}/policy.sha256" - -# Verify integrity of policy files -verify_policy_integrity() { - if [[ ! -f $INTEGRITY_FILE ]]; then - echo -e "${RED}SECURITY WARNING: Policy integrity file missing!${NC}" >&2 - echo -e "${RED}The pacman wrapper may have been tampered with.${NC}" >&2 - echo -e "${RED}Please reinstall the wrapper using: sudo install_pacman_wrapper.sh${NC}" >&2 - return 1 - fi - - local script_dir - script_dir="$(dirname "$(readlink -f "$0")")" - local blocked_file="$script_dir/pacman_blocked_keywords.txt" - local greylist_file="$script_dir/pacman_greylist.txt" - local whitelist_file="$script_dir/pacman_whitelist.txt" - - # Verify checksums - local failed=0 - while IFS= read -r line; do - local expected_hash expected_file - expected_hash=$(echo "$line" | awk '{print $1}') - expected_file=$(echo "$line" | awk '{print $2}') - - if [[ -f $expected_file ]]; then - local actual_hash - actual_hash=$(sha256sum "$expected_file" 2>/dev/null | awk '{print $1}') - if [[ $actual_hash != "$expected_hash" ]]; then - echo -e "${RED}SECURITY WARNING: Policy file integrity check failed for $expected_file${NC}" >&2 - failed=1 - fi - fi - done <"$INTEGRITY_FILE" - - if [[ $failed -eq 1 ]]; then - echo -e "${RED}CRITICAL: Policy files have been tampered with!${NC}" >&2 - echo -e "${RED}This could be an attempt to bypass security restrictions.${NC}" >&2 - echo -e "${RED}Wrapper operation DENIED. Please reinstall using: sudo install_pacman_wrapper.sh${NC}" >&2 - return 1 - fi - - return 0 -} - -# shellcheck disable=SC2329 # invoked indirectly, see is_blocked_package_name/is_greylisted_package_name callers below -load_policy_lists() { - if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then - return - fi - - local script_dir - script_dir="$(dirname "$(readlink -f "$0")")" - local blocked_file="$script_dir/pacman_blocked_keywords.txt" - local whitelist_file="$script_dir/pacman_whitelist.txt" - local greylist_file="$script_dir/pacman_greylist.txt" - - read_policy_list_file() { - local file_path="$1" - local -n target_array="$2" - local line="" - - target_array=() - while IFS= read -r line || [[ -n $line ]]; do - line="${line%$'\r'}" - if [[ $line =~ ^[[:space:]]*(#|$) ]]; then - continue - fi - target_array+=("${line,,}") - done <"$file_path" - } - - if [[ -f $blocked_file ]]; then - read_policy_list_file "$blocked_file" BLOCKED_KEYWORDS_LIST - else - BLOCKED_KEYWORDS_LIST=() - echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 - fi - - if [[ -f $whitelist_file ]]; then - read_policy_list_file "$whitelist_file" WHITELISTED_NAMES_LIST - else - WHITELISTED_NAMES_LIST=() - fi - - if [[ -f $greylist_file ]]; then - read_policy_list_file "$greylist_file" GREYLISTED_KEYWORDS_LIST - else - GREYLISTED_KEYWORDS_LIST=() - fi - - POLICY_LISTS_LOADED=1 -} -# Determine if this invocation may perform a transaction (upgrade/install/remove) -needs_unlock() { - # If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock - # Also include -Su/-Syu/-Syuu when -S is part of the combined flag - for arg in "$@"; do - case "$arg" in - -S*) - return 0 - ;; - -U) - return 0 - ;; - -R) - return 0 - ;; - --sync) - return 0 - ;; - --upgrade) - return 0 - ;; - --remove) - return 0 - ;; - esac - done - return 1 -} - -pacman_hooks_manage_guard_lib() { - local pre_hook="/etc/pacman.d/hooks/10-guard-lib-unlock-all.hook" - local post_hook="/etc/pacman.d/hooks/90-guard-lib-relock-all.hook" - local pre_exec="/etc/guard-lib/pacman-hooks/guard-lib-unlock-all.sh" - local post_exec="/etc/guard-lib/pacman-hooks/guard-lib-relock-all.sh" - - if [[ ! -f $pre_hook || ! -f $post_hook ]]; then - return 1 - fi - - grep -Fq "$pre_exec" "$pre_hook" && grep -Fq "$post_exec" "$post_hook" -} - -should_use_wrapper_guard_lib_fallback() { - if ! needs_unlock "$@"; then - return 1 - fi - - if pacman_hooks_manage_guard_lib; then - return 1 - fi - - return 0 -} - -# Run guard-lib's own generic unlock-all/relock-all scripts directly if -# pacman's own hooks for them are missing (e.g. hooks disabled/misconfigured). -# These cover every registered file-guard instance (hosts, nsswitch, -# resolved, shutdown-schedule, ...), not just /etc/hosts. -pre_unlock_guard_lib() { - local pre="/etc/guard-lib/pacman-hooks/guard-lib-unlock-all.sh" - if [[ -x $pre ]]; then - echo -e "${CYAN}[guard-lib] Preparing guarded files for transaction...${NC}" >&2 - /bin/bash "$pre" || true - fi -} - -post_relock_guard_lib() { - local post="/etc/guard-lib/pacman-hooks/guard-lib-relock-all.sh" - if [[ -x $post ]]; then - /bin/bash "$post" || true - echo -e "${CYAN}[guard-lib] Protections re-applied to guarded files.${NC}" >&2 - fi -} - -# Ensure periodic system services (timer/monitor) are set up; if not, trigger setup -ensure_periodic_maintenance() { - # Only proceed if systemd/systemctl is available - if ! command -v systemctl >/dev/null 2>&1; then - return 0 - fi - - local timer_unit="periodic-system-maintenance.timer" - local startup_unit="periodic-system-startup.service" - local monitor_unit="hosts-file-monitor.service" - local needs_setup=0 - - # Timer should be enabled and active - systemctl --quiet is-enabled "$timer_unit" || needs_setup=1 - systemctl --quiet is-active "$timer_unit" || needs_setup=1 - - # Monitor should be enabled and active - systemctl --quiet is-enabled "$monitor_unit" || needs_setup=1 - systemctl --quiet is-active "$monitor_unit" || needs_setup=1 - - # Startup service should be enabled (it’s oneshot and may not be active except at boot) - systemctl --quiet is-enabled "$startup_unit" || needs_setup=1 - - if [[ $needs_setup -eq 0 ]]; then - return 0 - fi - - echo -e "${YELLOW}Periodic maintenance services missing or inactive. Running setup...${NC}" >&2 - - # Try to locate setup_periodic_system.sh. The installed wrapper lives in - # /usr/local/bin (so $self_dir won't contain it) and, for real transactions, - # runs as root under sudo (so $HOME points at /root). Resolve the invoking - # user's home via SUDO_USER and probe the known repo locations. - local setup_script="" - local self_dir real_user real_home - self_dir="$(dirname "$(readlink -f "$0")")" - real_user="${SUDO_USER:-${USER:-$(id -un)}}" - real_home="$(getent passwd "$real_user" 2>/dev/null | cut -d: -f6)" - [[ -z $real_home ]] && real_home="$HOME" - - local -a setup_candidates=( - "$self_dir/setup_periodic_system.sh" - "$real_home/testsAndMisc/linux_configuration/scripts/periodic_background/setup_periodic_system.sh" - "$real_home/linux_configuration/scripts/periodic_background/setup_periodic_system.sh" - "$real_home/linux-configuration/scripts/periodic_background/setup_periodic_system.sh" - ) - local candidate - for candidate in "${setup_candidates[@]}"; do - if [[ -f $candidate ]]; then - setup_script="$candidate" - break - fi - done - - if [[ -n $setup_script ]]; then - if [[ $EUID -ne 0 ]]; then - sudo bash "$setup_script" - else - bash "$setup_script" - fi - echo -e "${CYAN}Tip:${NC} To disable these later:" >&2 - echo " sudo systemctl disable periodic-system-maintenance.timer" >&2 - echo " sudo systemctl disable periodic-system-startup.service" >&2 - echo " sudo systemctl disable hosts-file-monitor.service" >&2 - else - echo -e "${RED}Could not locate setup_periodic_system.sh to configure services automatically.${NC}" >&2 - fi -} - -# Function to display help -function show_help() { - echo -e "${BOLD}Pacman Wrapper Help${NC}" - echo "This wrapper adds helpful features while preserving all pacman functionality." - echo "" - echo "Additional commands:" - echo " --help-wrapper Show this help message" - echo " --makepkg-capped Run makepkg in a constrained systemd user scope" - echo " (forward remaining args to makepkg)" -} - -run_makepkg_capped() { - if [[ ! -x $MAKEPKG_CAPPED_BIN ]]; then - echo -e "${RED}makepkg capped wrapper not found at ${MAKEPKG_CAPPED_BIN}${NC}" >&2 - echo -e "${YELLOW}Run install_pacman_wrapper.sh to install it.${NC}" >&2 - return 1 - fi - - if [[ $EUID -eq 0 && -n ${SUDO_USER:-} ]]; then - exec sudo -u "$SUDO_USER" "$MAKEPKG_CAPPED_BIN" "$@" - fi - - exec "$MAKEPKG_CAPPED_BIN" "$@" -} - -# Function to display a message before executing -function display_operation() { - case "$1" in - -S) - echo -e "${BLUE}Installing packages...${NC}" >&2 - ;; - -Sy) - echo -e "${BLUE}Installing packages...${NC}" >&2 - ;; - -S\ *) - echo -e "${BLUE}Installing packages...${NC}" >&2 - ;; - -Syu | -Syyu) - echo -e "${BLUE}Updating system...${NC}" >&2 - ;; - -R) - echo -e "${YELLOW}Removing packages...${NC}" >&2 - ;; - -Rs) - echo -e "${YELLOW}Removing packages...${NC}" >&2 - ;; - -Rns) - echo -e "${YELLOW}Removing packages...${NC}" >&2 - ;; - -R\ *) - echo -e "${YELLOW}Removing packages...${NC}" >&2 - ;; - -Ss) - echo -e "${CYAN}Searching for packages...${NC}" >&2 - ;; - -Ss\ *) - echo -e "${CYAN}Searching for packages...${NC}" >&2 - ;; - -Q) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -Qs) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -Qi) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -Ql) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -Q\ *) - echo -e "${CYAN}Querying package database...${NC}" >&2 - ;; - -U) - echo -e "${BLUE}Installing local packages...${NC}" >&2 - ;; - -U\ *) - echo -e "${BLUE}Installing local packages...${NC}" >&2 - ;; - -Scc) - echo -e "${YELLOW}Cleaning package cache...${NC}" >&2 - ;; - *) - echo -e "${CYAN}Executing pacman command...${NC}" >&2 - ;; - esac -} - -# Helper: strip a pacman package-file suffix (---.pkg.tar.) -# down to the bare package name, if present. `pacman -U ` (what `yay` -# always uses to install a package it just built locally) passes a filename -# like "ungoogled-chromium-bin-150.0.7871.186-1-x86_64.pkg.tar.zst", not the -# bare name — without this, an exact-match whitelist entry for -# "ungoogled-chromium-bin" could never match a `-U` install and would always -# fall through to the (correctly still-substring) blocked-keywords check. -# Leaves package names without this suffix (the `-S` case) untouched. -# shellcheck disable=SC2329 # invoked indirectly by name (is_blocked_package_name) -function strip_pkgfile_suffix() { - local name="$1" - if [[ $name =~ ^(.+)-[^-]+-[0-9]+-(x86_64|any|i686|aarch64)\.pkg\.tar\.(zst|xz|gz|bz2|lrz|lzo|Z)$ ]]; then - printf '%s' "${BASH_REMATCH[1]}" - else - printf '%s' "$name" - fi -} - -# Helper: return 0 if the given package name is blocked by policy -# shellcheck disable=SC2329 # invoked indirectly by name (remove_installed_packages_matching, check_install_for) -function is_blocked_package_name() { - load_policy_lists - local normalized="${1,,}" - local bare_name - bare_name="$(strip_pkgfile_suffix "$normalized")" - - for allowed in "${WHITELISTED_NAMES_LIST[@]}"; do - if [[ $normalized == "$allowed" || $bare_name == "$allowed" ]]; then - return 1 - fi - done - - for keyword in "${BLOCKED_KEYWORDS_LIST[@]}"; do - if [[ $normalized == *"$keyword"* ]]; then - return 0 - fi - done - - return 1 -} - -# Helper: return 0 if the given package name is greylisted (challenge required) -# shellcheck disable=SC2329 # invoked indirectly by name (remove_installed_packages_matching, check_install_for) -function is_greylisted_package_name() { - load_policy_lists - local normalized="${1,,}" - - for keyword in "${GREYLISTED_KEYWORDS_LIST[@]}"; do - if [[ $normalized == *"$keyword"* ]]; then - return 0 - fi - done - - return 1 -} - -current_day_of_week() { - printf '%(%u)T\n' -1 -} - -current_hour_24() { - printf '%(%H)T\n' -1 -} - -current_day_name() { - printf '%(%A)T\n' -1 -} +# The policy lists and the integrity-file paths are declared in pw_policy.sh, +# the only file that reads or writes them. # Stale pacman DB-lock detection/handling (get_lock_holders, # check_and_handle_db_lock) plus has_noconfirm_flag and current_epoch now live in # the shared pacman_lock_lib.sh, sourced after the integrity check below. This is # the single source of truth shared with makepkg_wrapper.sh. -# Generic function to remove installed packages matching a filter -# Args: check_function label_prefix -function remove_installed_packages_matching() { - local check_function="$1" - local label="$2" - - mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null) - local to_remove=() - for name in "${installed_names[@]}"; do - if "$check_function" "$name"; then - to_remove+=("$name") - fi - done - - if [[ ${#to_remove[@]} -eq 0 ]]; then - return 0 - fi - - echo -e "${YELLOW}${label} cleanup:${NC} Removing packages: ${BOLD}${to_remove[*]}${NC}" >&2 - "$PACMAN_BIN" -Rns --noconfirm "${to_remove[@]}" - local rc=$? - if [[ $rc -ne 0 ]]; then - echo -e "${RED}${label} cleanup removal failed with exit code ${rc}.${NC}" >&2 - else - echo -e "${GREEN}${label} cleanup removal completed for: ${to_remove[*]}${NC}" >&2 - fi - return $rc -} - -# Cleanup: remove any installed blocked packages -function remove_installed_blocked_packages() { - remove_installed_packages_matching is_blocked_package_name "Policy" -} - -# Cleanup: remove any installed greylisted packages -function remove_installed_greylisted_packages() { - remove_installed_packages_matching is_greylisted_package_name "Greylist" -} - -# Helper: Check if this is an install command and run a filter on each package name -# Usage: check_install_for filter_func "$@" -# Returns 0 if filter_func matches any package -function check_install_for() { - local filter_func="$1" - shift - # Check if the command is an installation command - if [[ ${1:-} == "-S" || ${1:-} == "-Sy" || ${1:-} == "-Syu" || ${1:-} == "-Syyu" || ${1:-} == "-U" ]]; then - for arg in "$@"; do - # Strip repository prefix if present (like extra/ or community/) - local package_name="${arg##*/}" - if "$filter_func" "$package_name"; then - return 0 - fi - done - fi - return 1 -} - -# Function to check if user is trying to install packages that are always blocked -function check_for_always_blocked() { - check_install_for is_blocked_package_name "$@" -} - -# Helper to check if a package name is steam -# shellcheck disable=SC2329 # invoked indirectly by name (check_install_for) -function is_steam_package() { - [[ $1 == "steam" ]] -} - -# Function to check if user is trying to install steam (challenge-eligible package) -function check_for_steam() { - check_install_for is_steam_package "$@" -} - -# Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) -function is_weekday() { - local day_of_week - day_of_week=$(current_day_of_week) # %u gives 1-7 (Monday is 1, Sunday is 7) - local hour - hour=$(current_hour_24) # %H gives hour in 24-hour format (00-23) - - # Monday through Thursday are always weekdays - if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then - return 0 # Is weekday - # Friday before 4PM is weekday, after 4PM is weekend - elif [[ $day_of_week -eq 5 ]]; then - if [[ $hour -lt 14 ]]; then - return 0 # Is weekday (Friday before 4PM) - else - return 1 # Is weekend (Friday after 4PM) - fi - # Saturday and Sunday are weekend - else - return 1 # Is weekend - fi -} - -# Unified word unscrambling challenge function -# Args: challenge_name word_length words_count timeout_seconds initial_delay_max post_delay_min post_delay_range -function run_word_challenge() { - local challenge_name="$1" - local word_length="$2" - local words_count="$3" - local timeout_seconds="$4" - local initial_delay_max="${5:-20}" - local post_delay_min="${6:-0}" - local post_delay_range="${7:-20}" - - echo -e "${YELLOW}${challenge_name} challenge will begin shortly...${NC}" - - # Initial delay - local sleep_duration=$((RANDOM % initial_delay_max)) - sleep "$sleep_duration" - - # Load words file - local script_dir words_file - script_dir="$(dirname "$(readlink -f "$0")")" - words_file="$script_dir/words.txt" - - if [[ ! -f $words_file ]]; then - echo -e "${RED}Error: words.txt file not found at $words_file${NC}" - return 1 - fi - - echo -e "${CYAN}Challenge: Words with ${word_length} letters${NC}" - - # Load random words of specified length - local -a selected_words - mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") - - if [[ ${#selected_words[@]} -lt $words_count ]]; then - echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" - words_count=${#selected_words[@]} - if [[ $words_count -eq 0 ]]; then - echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" - return 1 - fi - fi - - # Convert to uppercase - for i in "${!selected_words[@]}"; do - selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') - done - - echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" - - # Display words in grid - for ((i = 0; i < words_count; i++)); do - printf "${BLUE}%-15s${NC}" "${selected_words[i]}" - if (((i + 1) % 4 == 0)); then - echo "" - fi - done - - # Select and scramble a word - local target_index target_word scrambled_word - target_index=$((RANDOM % words_count)) - target_word="${selected_words[target_index]}" - scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') - - if [[ $scrambled_word == "$target_word" ]]; then - scrambled_word=$(echo "$target_word" | rev) - fi - - echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" - echo -e "${YELLOW}Unscramble the word to proceed (you have $timeout_seconds seconds):${NC}" - - # Timer display background process - ( - local start_time current_time elapsed remaining - start_time=$(current_epoch) - while true; do - current_time=$(current_epoch) - elapsed=$((current_time - start_time)) - remaining=$((timeout_seconds - elapsed)) - if [[ $remaining -le 0 ]]; then - echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " - break - fi - echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " - sleep 1 - done - ) & - local display_pid=$! - - # Read input with timeout - local user_input read_status - read -t "$timeout_seconds" -r user_input - read_status=$? - - kill "$display_pid" 2>/dev/null - wait "$display_pid" 2>/dev/null - echo - - if [[ $read_status -ne 0 ]]; then - echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}" - return 1 - fi - - user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) - - if [[ $user_input == "$target_word" ]]; then - echo -e "${GREEN}Correct! Proceeding with installation...${NC}" - local post_challenge_sleep=$((RANDOM % post_delay_range + post_delay_min)) - [[ $post_challenge_sleep -gt 0 ]] && sleep "$post_challenge_sleep" - return 0 - else - echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}" - return 1 - fi -} - -# Function to prompt for solving a word unscrambling challenge (only for steam) -function prompt_for_steam_challenge() { - echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" - - # Check if it's a weekday and block completely - if is_weekday; then - local day_name - day_name=$(current_day_name) - echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" - echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" - return 1 - fi - - # word_length=5, words_count=160, timeout=60s, initial_delay=20, post_delay=0-20 - run_word_challenge "Weekend Steam" 5 160 60 20 0 20 -} - -function check_for_greylisted() { - check_install_for is_greylisted_package_name "$@" -} - -# Function to prompt for solving a word unscrambling challenge (for greylisted packages - always active) -function prompt_for_greylist_challenge() { - echo -e "${YELLOW}WARNING: You are trying to install a greylisted package.${NC}" - - # word_length=6, words_count=120, timeout=90s, initial_delay=30, post_delay=15-35 - run_word_challenge "Greylist" 6 120 90 30 15 20 -} - # Check for wrapper-specific commands if [[ $1 == "--help-wrapper" ]]; then show_help @@ -699,7 +98,6 @@ fi # has_noconfirm_flag, current_epoch), AFTER integrity verification so a tampered # lib (listed in the integrity manifest) is caught before it is sourced. Sourced # here — past the fast-path — so build-time non-root calls skip it entirely. -_WRAPPER_DIR="$(dirname "$(readlink -f "$0")")" if [[ -r "$_WRAPPER_DIR/pacman_lock_lib.sh" ]]; then # shellcheck source=pacman_lock_lib.sh source "$_WRAPPER_DIR/pacman_lock_lib.sh" @@ -801,120 +199,8 @@ remove_installed_blocked_packages "$@" # Also remove installed greylisted packages remove_installed_greylisted_packages "$@" -# Auto-install LeechBlock if a browser is detected -auto_install_leechblock() { - # Only check after install operations - if [[ -z ${1:-} ]] || [[ $1 != "-S"* && $1 != "-U"* ]]; then - return 0 - fi - - # List of browser packages to check for - local browsers=("firefox" "librewolf" "chromium" "brave" "vivaldi" "google-chrome" "ungoogled-chromium") - local browser_found=0 - - for browser in "${browsers[@]}"; do - if "$PACMAN_BIN" -Qq "$browser" 2>/dev/null; then - browser_found=1 - break - fi - done - - if [[ $browser_found -eq 0 ]]; then - return 0 - fi - - # Find the LeechBlock installer - local script_dir - script_dir="$(dirname "$(readlink -f "$0")")" - local leechblock_installer="" - - if [[ -f "/usr/local/share/digital_wellbeing/install_leechblock.sh" ]]; then - leechblock_installer="/usr/local/share/digital_wellbeing/install_leechblock.sh" - elif [[ -f "$script_dir/../install_leechblock.sh" ]]; then - leechblock_installer="$script_dir/../install_leechblock.sh" - fi - - if [[ -z $leechblock_installer ]]; then - echo -e "${YELLOW}Browser detected but LeechBlock installer not found.${NC}" >&2 - return 0 - fi - - # Check if LeechBlock is already installed (by looking for the extension directory) - if [[ -d "$HOME/.local/share/leechblockng" ]]; then - return 0 - fi - - echo -e "${CYAN}Browser detected. Installing LeechBlock extension for website blocking...${NC}" >&2 - - # Run the LeechBlock installer (as current user, not root) - if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" ]]; then - sudo -u "$SUDO_USER" bash "$leechblock_installer" --install-firefox 2>&1 || { - echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2 - echo -e "${YELLOW} $leechblock_installer${NC}" >&2 - } - else - bash "$leechblock_installer" --install-firefox 2>&1 || { - echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2 - echo -e "${YELLOW} $leechblock_installer${NC}" >&2 - } - fi -} - auto_install_leechblock "$@" -# If VirtualBox is installed, automatically remove all VMs -auto_remove_virtualbox_vms() { - # Check if VBoxManage is available - if ! command -v VBoxManage &>/dev/null; then - return 0 - fi - - # Determine real user (wrapper may run as root via sudo) - local real_user="${SUDO_USER:-$USER}" - - # Get list of registered VMs (run as real user since VMs are per-user) - local vm_list - vm_list=$(sudo -u "$real_user" VBoxManage list vms 2>/dev/null) || return 0 - - if [[ -z $vm_list ]]; then - return 0 - fi - - echo -e "${RED}═══════════════════════════════════════════════════════${NC}" >&2 - echo -e "${RED} VIRTUALBOX VMs DETECTED - AUTO-REMOVING ${NC}" >&2 - echo -e "${RED}═══════════════════════════════════════════════════════${NC}" >&2 - - local vm_name - local success=0 - local failed=0 - - while IFS= read -r line; do - # VBoxManage list vms output format: "VM Name" {uuid} - vm_name="${line#\"}" - vm_name="${vm_name%%\"*}" - if [[ -z $vm_name ]]; then - continue - fi - - echo -e "${YELLOW}Removing VM: ${vm_name}${NC}" >&2 - - # Power off the VM if it's running - sudo -u "$real_user" VBoxManage controlvm "$vm_name" poweroff 2>/dev/null || true - sleep 1 - - # Unregister and delete all files - if sudo -u "$real_user" VBoxManage unregistervm "$vm_name" --delete 2>/dev/null; then - echo -e "${GREEN} Removed: ${vm_name}${NC}" >&2 - ((++success)) - else - echo -e "${RED} Failed to remove: ${vm_name}${NC}" >&2 - ((++failed)) - fi - done <<<"$vm_list" - - echo -e "${CYAN}VM removal complete: ${success} removed, ${failed} failed.${NC}" >&2 -} - auto_remove_virtualbox_vms # Display some helpful tips depending on the operation diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_challenge.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_challenge.sh new file mode 100755 index 00000000..b1532d30 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_challenge.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# Helpers sourced by the entry script. + +current_day_of_week() { + printf '%(%u)T\n' -1 +} + +current_hour_24() { + printf '%(%H)T\n' -1 +} + +current_day_name() { + printf '%(%A)T\n' -1 +} + +# Function to check if current day is a weekday (after 4PM Friday until midnight Sunday) +function is_weekday() { + local day_of_week + day_of_week=$(current_day_of_week) # %u gives 1-7 (Monday is 1, Sunday is 7) + local hour + hour=$(current_hour_24) # %H gives hour in 24-hour format (00-23) + + # Monday through Thursday are always weekdays + if [[ $day_of_week -ge 1 && $day_of_week -le 4 ]]; then + return 0 # Is weekday + # Friday before 4PM is weekday, after 4PM is weekend + elif [[ $day_of_week -eq 5 ]]; then + if [[ $hour -lt 14 ]]; then + return 0 # Is weekday (Friday before 4PM) + else + return 1 # Is weekend (Friday after 4PM) + fi + # Saturday and Sunday are weekend + else + return 1 # Is weekend + fi +} + +# Unified word unscrambling challenge function +# Args: challenge_name word_length words_count timeout_seconds initial_delay_max post_delay_min post_delay_range +function run_word_challenge() { + local challenge_name="$1" + local word_length="$2" + local words_count="$3" + local timeout_seconds="$4" + local initial_delay_max="${5:-20}" + local post_delay_min="${6:-0}" + local post_delay_range="${7:-20}" + + echo -e "${YELLOW}${challenge_name} challenge will begin shortly...${NC}" + + # Initial delay + local sleep_duration=$((RANDOM % initial_delay_max)) + sleep "$sleep_duration" + + # Load words file + local script_dir words_file + script_dir="$(dirname "$(readlink -f "$0")")" + words_file="$script_dir/words.txt" + + if [[ ! -f $words_file ]]; then + echo -e "${RED}Error: words.txt file not found at $words_file${NC}" + return 1 + fi + + echo -e "${CYAN}Challenge: Words with ${word_length} letters${NC}" + + # Load random words of specified length + local -a selected_words + mapfile -t selected_words < <(grep -E "^[a-zA-Z]{$word_length}$" "$words_file" | shuf -n "$words_count") + + if [[ ${#selected_words[@]} -lt $words_count ]]; then + echo -e "${RED}Warning: Could only find ${#selected_words[@]} words of length $word_length.${NC}" + words_count=${#selected_words[@]} + if [[ $words_count -eq 0 ]]; then + echo -e "${RED}Error: No words of length $word_length found in $words_file${NC}" + return 1 + fi + fi + + # Convert to uppercase + for i in "${!selected_words[@]}"; do + selected_words[i]=$(echo "${selected_words[i]}" | tr '[:lower:]' '[:upper:]') + done + + echo -e "${CYAN}Here are ${words_count} random words. Remember them:${NC}" + + # Display words in grid + for ((i = 0; i < words_count; i++)); do + printf "${BLUE}%-15s${NC}" "${selected_words[i]}" + if (((i + 1) % 4 == 0)); then + echo "" + fi + done + + # Select and scramble a word + local target_index target_word scrambled_word + target_index=$((RANDOM % words_count)) + target_word="${selected_words[target_index]}" + scrambled_word=$(echo "$target_word" | fold -w1 | shuf | tr -d '\n') + + if [[ $scrambled_word == "$target_word" ]]; then + scrambled_word=$(echo "$target_word" | rev) + fi + + echo -e "\n${YELLOW}One of those words has been scrambled to:${NC} ${CYAN}$scrambled_word${NC}" + echo -e "${YELLOW}Unscramble the word to proceed (you have $timeout_seconds seconds):${NC}" + + # Timer display background process + ( + local start_time current_time elapsed remaining + start_time=$(current_epoch) + while true; do + current_time=$(current_epoch) + elapsed=$((current_time - start_time)) + remaining=$((timeout_seconds - elapsed)) + if [[ $remaining -le 0 ]]; then + echo -ne "\r${YELLOW}Time remaining: 0 seconds${NC} " + break + fi + echo -ne "\r${YELLOW}Time remaining: ${remaining} seconds${NC} " + sleep 1 + done + ) & + local display_pid=$! + + # Read input with timeout + local user_input read_status + read -t "$timeout_seconds" -r user_input + read_status=$? + + kill "$display_pid" 2>/dev/null + wait "$display_pid" 2>/dev/null + echo + + if [[ $read_status -ne 0 ]]; then + echo -e "${RED}Time's up! Challenge failed. The correct word was '$target_word'.${NC}" + return 1 + fi + + user_input=$(echo "$user_input" | tr '[:lower:]' '[:upper:]' | xargs) + + if [[ $user_input == "$target_word" ]]; then + echo -e "${GREEN}Correct! Proceeding with installation...${NC}" + local post_challenge_sleep=$((RANDOM % post_delay_range + post_delay_min)) + [[ $post_challenge_sleep -gt 0 ]] && sleep "$post_challenge_sleep" + return 0 + else + echo -e "${RED}Incorrect answer. Installation aborted. The correct word was '$target_word'.${NC}" + return 1 + fi +} + +# Function to prompt for solving a word unscrambling challenge (only for steam) +function prompt_for_steam_challenge() { + echo -e "${YELLOW}WARNING: You are trying to install Steam.${NC}" + + # Check if it's a weekday and block completely + if is_weekday; then + local day_name + day_name=$(current_day_name) + echo -e "${RED}Steam installation BLOCKED: Steam cannot be installed on weekdays.${NC}" + echo -e "${RED}Today is $day_name. Please try again on the weekend (Saturday or Sunday).${NC}" + return 1 + fi + + # word_length=5, words_count=160, timeout=60s, initial_delay=20, post_delay=0-20 + run_word_challenge "Weekend Steam" 5 160 60 20 0 20 +} + +# Function to prompt for solving a word unscrambling challenge (for greylisted packages - always active) +function prompt_for_greylist_challenge() { + echo -e "${YELLOW}WARNING: You are trying to install a greylisted package.${NC}" + + # word_length=6, words_count=120, timeout=90s, initial_delay=30, post_delay=15-35 + run_word_challenge "Greylist" 6 120 90 30 15 20 +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_cleanup.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_cleanup.sh new file mode 100755 index 00000000..96da64f4 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_cleanup.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Helpers sourced by the entry script. + +# Generic function to remove installed packages matching a filter +# Args: check_function label_prefix +function remove_installed_packages_matching() { + local check_function="$1" + local label="$2" + + mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null) + local to_remove=() + for name in "${installed_names[@]}"; do + if "$check_function" "$name"; then + to_remove+=("$name") + fi + done + + if [[ ${#to_remove[@]} -eq 0 ]]; then + return 0 + fi + + echo -e "${YELLOW}${label} cleanup:${NC} Removing packages: ${BOLD}${to_remove[*]}${NC}" >&2 + "$PACMAN_BIN" -Rns --noconfirm "${to_remove[@]}" + local rc=$? + if [[ $rc -ne 0 ]]; then + echo -e "${RED}${label} cleanup removal failed with exit code ${rc}.${NC}" >&2 + else + echo -e "${GREEN}${label} cleanup removal completed for: ${to_remove[*]}${NC}" >&2 + fi + return $rc +} + +# Cleanup: remove any installed blocked packages +function remove_installed_blocked_packages() { + remove_installed_packages_matching is_blocked_package_name "Policy" +} + +# Cleanup: remove any installed greylisted packages +function remove_installed_greylisted_packages() { + remove_installed_packages_matching is_greylisted_package_name "Greylist" +} + +# Auto-install LeechBlock if a browser is detected +auto_install_leechblock() { + # Only check after install operations + if [[ -z ${1:-} ]] || [[ $1 != "-S"* && $1 != "-U"* ]]; then + return 0 + fi + + # List of browser packages to check for + local browsers=("firefox" "librewolf" "chromium" "brave" "vivaldi" "google-chrome" "ungoogled-chromium") + local browser_found=0 + + for browser in "${browsers[@]}"; do + if "$PACMAN_BIN" -Qq "$browser" 2>/dev/null; then + browser_found=1 + break + fi + done + + if [[ $browser_found -eq 0 ]]; then + return 0 + fi + + # Find the LeechBlock installer + local script_dir + script_dir="$(dirname "$(readlink -f "$0")")" + local leechblock_installer="" + + if [[ -f "/usr/local/share/digital_wellbeing/install_leechblock.sh" ]]; then + leechblock_installer="/usr/local/share/digital_wellbeing/install_leechblock.sh" + elif [[ -f "$script_dir/../install_leechblock.sh" ]]; then + leechblock_installer="$script_dir/../install_leechblock.sh" + fi + + if [[ -z $leechblock_installer ]]; then + echo -e "${YELLOW}Browser detected but LeechBlock installer not found.${NC}" >&2 + return 0 + fi + + # Check if LeechBlock is already installed (by looking for the extension directory) + if [[ -d "$HOME/.local/share/leechblockng" ]]; then + return 0 + fi + + echo -e "${CYAN}Browser detected. Installing LeechBlock extension for website blocking...${NC}" >&2 + + # Run the LeechBlock installer (as current user, not root) + if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" ]]; then + sudo -u "$SUDO_USER" bash "$leechblock_installer" --install-firefox 2>&1 || { + echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2 + echo -e "${YELLOW} $leechblock_installer${NC}" >&2 + } + else + bash "$leechblock_installer" --install-firefox 2>&1 || { + echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2 + echo -e "${YELLOW} $leechblock_installer${NC}" >&2 + } + fi +} + +# If VirtualBox is installed, automatically remove all VMs +auto_remove_virtualbox_vms() { + # Check if VBoxManage is available + if ! command -v VBoxManage &>/dev/null; then + return 0 + fi + + # Determine real user (wrapper may run as root via sudo) + local real_user="${SUDO_USER:-$USER}" + + # Get list of registered VMs (run as real user since VMs are per-user) + local vm_list + vm_list=$(sudo -u "$real_user" VBoxManage list vms 2>/dev/null) || return 0 + + if [[ -z $vm_list ]]; then + return 0 + fi + + echo -e "${RED}═══════════════════════════════════════════════════════${NC}" >&2 + echo -e "${RED} VIRTUALBOX VMs DETECTED - AUTO-REMOVING ${NC}" >&2 + echo -e "${RED}═══════════════════════════════════════════════════════${NC}" >&2 + + local vm_name + local success=0 + local failed=0 + + while IFS= read -r line; do + # VBoxManage list vms output format: "VM Name" {uuid} + vm_name="${line#\"}" + vm_name="${vm_name%%\"*}" + if [[ -z $vm_name ]]; then + continue + fi + + echo -e "${YELLOW}Removing VM: ${vm_name}${NC}" >&2 + + # Power off the VM if it's running + sudo -u "$real_user" VBoxManage controlvm "$vm_name" poweroff 2>/dev/null || true + sleep 1 + + # Unregister and delete all files + if sudo -u "$real_user" VBoxManage unregistervm "$vm_name" --delete 2>/dev/null; then + echo -e "${GREEN} Removed: ${vm_name}${NC}" >&2 + ((++success)) + else + echo -e "${RED} Failed to remove: ${vm_name}${NC}" >&2 + ((++failed)) + fi + done <<<"$vm_list" + + echo -e "${CYAN}VM removal complete: ${success} removed, ${failed} failed.${NC}" >&2 +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_guard.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_guard.sh new file mode 100755 index 00000000..6ea5ce31 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_guard.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# Helpers sourced by the entry script. + +# Determine if this invocation may perform a transaction (upgrade/install/remove) +needs_unlock() { + # If args include -S (install/upgrade), -U (local install), or -R (remove), we unlock + # Also include -Su/-Syu/-Syuu when -S is part of the combined flag + for arg in "$@"; do + case "$arg" in + -S*) + return 0 + ;; + -U) + return 0 + ;; + -R) + return 0 + ;; + --sync) + return 0 + ;; + --upgrade) + return 0 + ;; + --remove) + return 0 + ;; + esac + done + return 1 +} + +pacman_hooks_manage_guard_lib() { + local pre_hook="/etc/pacman.d/hooks/10-guard-lib-unlock-all.hook" + local post_hook="/etc/pacman.d/hooks/90-guard-lib-relock-all.hook" + local pre_exec="/etc/guard-lib/pacman-hooks/guard-lib-unlock-all.sh" + local post_exec="/etc/guard-lib/pacman-hooks/guard-lib-relock-all.sh" + + if [[ ! -f $pre_hook || ! -f $post_hook ]]; then + return 1 + fi + + grep -Fq "$pre_exec" "$pre_hook" && grep -Fq "$post_exec" "$post_hook" +} + +should_use_wrapper_guard_lib_fallback() { + if ! needs_unlock "$@"; then + return 1 + fi + + if pacman_hooks_manage_guard_lib; then + return 1 + fi + + return 0 +} + +# Run guard-lib's own generic unlock-all/relock-all scripts directly if +# pacman's own hooks for them are missing (e.g. hooks disabled/misconfigured). +# These cover every registered file-guard instance (hosts, nsswitch, +# resolved, shutdown-schedule, ...), not just /etc/hosts. +pre_unlock_guard_lib() { + local pre="/etc/guard-lib/pacman-hooks/guard-lib-unlock-all.sh" + if [[ -x $pre ]]; then + echo -e "${CYAN}[guard-lib] Preparing guarded files for transaction...${NC}" >&2 + /bin/bash "$pre" || true + fi +} + +post_relock_guard_lib() { + local post="/etc/guard-lib/pacman-hooks/guard-lib-relock-all.sh" + if [[ -x $post ]]; then + /bin/bash "$post" || true + echo -e "${CYAN}[guard-lib] Protections re-applied to guarded files.${NC}" >&2 + fi +} + +# Ensure periodic system services (timer/monitor) are set up; if not, trigger setup +ensure_periodic_maintenance() { + # Only proceed if systemd/systemctl is available + if ! command -v systemctl >/dev/null 2>&1; then + return 0 + fi + + local timer_unit="periodic-system-maintenance.timer" + local startup_unit="periodic-system-startup.service" + local monitor_unit="hosts-file-monitor.service" + local needs_setup=0 + + # Timer should be enabled and active + systemctl --quiet is-enabled "$timer_unit" || needs_setup=1 + systemctl --quiet is-active "$timer_unit" || needs_setup=1 + + # Monitor should be enabled and active + systemctl --quiet is-enabled "$monitor_unit" || needs_setup=1 + systemctl --quiet is-active "$monitor_unit" || needs_setup=1 + + # Startup service should be enabled (it’s oneshot and may not be active except at boot) + systemctl --quiet is-enabled "$startup_unit" || needs_setup=1 + + if [[ $needs_setup -eq 0 ]]; then + return 0 + fi + + echo -e "${YELLOW}Periodic maintenance services missing or inactive. Running setup...${NC}" >&2 + + # Try to locate setup_periodic_system.sh. The installed wrapper lives in + # /usr/local/bin (so $self_dir won't contain it) and, for real transactions, + # runs as root under sudo (so $HOME points at /root). Resolve the invoking + # user's home via SUDO_USER and probe the known repo locations. + local setup_script="" + local self_dir real_user real_home + self_dir="$(dirname "$(readlink -f "$0")")" + real_user="${SUDO_USER:-${USER:-$(id -un)}}" + real_home="$(getent passwd "$real_user" 2>/dev/null | cut -d: -f6)" + [[ -z $real_home ]] && real_home="$HOME" + + local -a setup_candidates=( + "$self_dir/setup_periodic_system.sh" + "$real_home/testsAndMisc/linux_configuration/scripts/periodic_background/setup_periodic_system.sh" + "$real_home/linux_configuration/scripts/periodic_background/setup_periodic_system.sh" + "$real_home/linux-configuration/scripts/periodic_background/setup_periodic_system.sh" + ) + local candidate + for candidate in "${setup_candidates[@]}"; do + if [[ -f $candidate ]]; then + setup_script="$candidate" + break + fi + done + + if [[ -n $setup_script ]]; then + if [[ $EUID -ne 0 ]]; then + sudo bash "$setup_script" + else + bash "$setup_script" + fi + echo -e "${CYAN}Tip:${NC} To disable these later:" >&2 + echo " sudo systemctl disable periodic-system-maintenance.timer" >&2 + echo " sudo systemctl disable periodic-system-startup.service" >&2 + echo " sudo systemctl disable hosts-file-monitor.service" >&2 + else + echo -e "${RED}Could not locate setup_periodic_system.sh to configure services automatically.${NC}" >&2 + fi +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_policy.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_policy.sh new file mode 100755 index 00000000..d976a969 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_policy.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# Helpers sourced by the entry script. + +# Declared here rather than in the entry script: this is the only file that +# reads or writes them, and a cross-file global the sourcing script cannot +# see is a genuine SC2034 (the hook runs shellcheck without -x). +declare -a BLOCKED_KEYWORDS_LIST=() +declare -a WHITELISTED_NAMES_LIST=() +declare -a GREYLISTED_KEYWORDS_LIST=() +POLICY_LISTS_LOADED=0 +INTEGRITY_DIR="/var/lib/pacman-wrapper" +INTEGRITY_FILE="${INTEGRITY_DIR}/policy.sha256" + +# Verify integrity of policy files +verify_policy_integrity() { + if [[ ! -f $INTEGRITY_FILE ]]; then + echo -e "${RED}SECURITY WARNING: Policy integrity file missing!${NC}" >&2 + echo -e "${RED}The pacman wrapper may have been tampered with.${NC}" >&2 + echo -e "${RED}Please reinstall the wrapper using: sudo install_pacman_wrapper.sh${NC}" >&2 + return 1 + fi + + local script_dir + script_dir="$(dirname "$(readlink -f "$0")")" + local blocked_file="$script_dir/pacman_blocked_keywords.txt" + local greylist_file="$script_dir/pacman_greylist.txt" + local whitelist_file="$script_dir/pacman_whitelist.txt" + + # Verify checksums + local failed=0 + while IFS= read -r line; do + local expected_hash expected_file + expected_hash=$(echo "$line" | awk '{print $1}') + expected_file=$(echo "$line" | awk '{print $2}') + + if [[ -f $expected_file ]]; then + local actual_hash + actual_hash=$(sha256sum "$expected_file" 2>/dev/null | awk '{print $1}') + if [[ $actual_hash != "$expected_hash" ]]; then + echo -e "${RED}SECURITY WARNING: Policy file integrity check failed for $expected_file${NC}" >&2 + failed=1 + fi + fi + done <"$INTEGRITY_FILE" + + if [[ $failed -eq 1 ]]; then + echo -e "${RED}CRITICAL: Policy files have been tampered with!${NC}" >&2 + echo -e "${RED}This could be an attempt to bypass security restrictions.${NC}" >&2 + echo -e "${RED}Wrapper operation DENIED. Please reinstall using: sudo install_pacman_wrapper.sh${NC}" >&2 + return 1 + fi + + return 0 +} + +# shellcheck disable=SC2329 # invoked indirectly, see is_blocked_package_name/is_greylisted_package_name callers below +load_policy_lists() { + if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then + return + fi + + local script_dir + script_dir="$(dirname "$(readlink -f "$0")")" + local blocked_file="$script_dir/pacman_blocked_keywords.txt" + local whitelist_file="$script_dir/pacman_whitelist.txt" + local greylist_file="$script_dir/pacman_greylist.txt" + + read_policy_list_file() { + local file_path="$1" + local -n target_array="$2" + local line="" + + target_array=() + while IFS= read -r line || [[ -n $line ]]; do + line="${line%$'\r'}" + if [[ $line =~ ^[[:space:]]*(#|$) ]]; then + continue + fi + target_array+=("${line,,}") + done <"$file_path" + } + + if [[ -f $blocked_file ]]; then + read_policy_list_file "$blocked_file" BLOCKED_KEYWORDS_LIST + else + BLOCKED_KEYWORDS_LIST=() + echo -e "${YELLOW}Warning:${NC} Missing blocked keywords file at $blocked_file" >&2 + fi + + if [[ -f $whitelist_file ]]; then + read_policy_list_file "$whitelist_file" WHITELISTED_NAMES_LIST + else + WHITELISTED_NAMES_LIST=() + fi + + if [[ -f $greylist_file ]]; then + read_policy_list_file "$greylist_file" GREYLISTED_KEYWORDS_LIST + else + GREYLISTED_KEYWORDS_LIST=() + fi + + POLICY_LISTS_LOADED=1 +} + +# Helper: strip a pacman package-file suffix (---.pkg.tar.) +# down to the bare package name, if present. `pacman -U ` (what `yay` +# always uses to install a package it just built locally) passes a filename +# like "ungoogled-chromium-bin-150.0.7871.186-1-x86_64.pkg.tar.zst", not the +# bare name — without this, an exact-match whitelist entry for +# "ungoogled-chromium-bin" could never match a `-U` install and would always +# fall through to the (correctly still-substring) blocked-keywords check. +# Leaves package names without this suffix (the `-S` case) untouched. +# shellcheck disable=SC2329 # invoked indirectly by name (is_blocked_package_name) +function strip_pkgfile_suffix() { + local name="$1" + if [[ $name =~ ^(.+)-[^-]+-[0-9]+-(x86_64|any|i686|aarch64)\.pkg\.tar\.(zst|xz|gz|bz2|lrz|lzo|Z)$ ]]; then + printf '%s' "${BASH_REMATCH[1]}" + else + printf '%s' "$name" + fi +} + +# Helper: return 0 if the given package name is blocked by policy +# shellcheck disable=SC2329 # invoked indirectly by name (remove_installed_packages_matching, check_install_for) +function is_blocked_package_name() { + load_policy_lists + local normalized="${1,,}" + local bare_name + bare_name="$(strip_pkgfile_suffix "$normalized")" + + for allowed in "${WHITELISTED_NAMES_LIST[@]}"; do + if [[ $normalized == "$allowed" || $bare_name == "$allowed" ]]; then + return 1 + fi + done + + for keyword in "${BLOCKED_KEYWORDS_LIST[@]}"; do + if [[ $normalized == *"$keyword"* ]]; then + return 0 + fi + done + + return 1 +} + +# Helper: return 0 if the given package name is greylisted (challenge required) +# shellcheck disable=SC2329 # invoked indirectly by name (remove_installed_packages_matching, check_install_for) +function is_greylisted_package_name() { + load_policy_lists + local normalized="${1,,}" + + for keyword in "${GREYLISTED_KEYWORDS_LIST[@]}"; do + if [[ $normalized == *"$keyword"* ]]; then + return 0 + fi + done + + return 1 +} + +# Helper: Check if this is an install command and run a filter on each package name +# Usage: check_install_for filter_func "$@" +# Returns 0 if filter_func matches any package +function check_install_for() { + local filter_func="$1" + shift + # Check if the command is an installation command + if [[ ${1:-} == "-S" || ${1:-} == "-Sy" || ${1:-} == "-Syu" || ${1:-} == "-Syyu" || ${1:-} == "-U" ]]; then + for arg in "$@"; do + # Strip repository prefix if present (like extra/ or community/) + local package_name="${arg##*/}" + if "$filter_func" "$package_name"; then + return 0 + fi + done + fi + return 1 +} + +# Function to check if user is trying to install packages that are always blocked +function check_for_always_blocked() { + check_install_for is_blocked_package_name "$@" +} + +# Helper to check if a package name is steam +# shellcheck disable=SC2329 # invoked indirectly by name (check_install_for) +function is_steam_package() { + [[ $1 == "steam" ]] +} + +# Function to check if user is trying to install steam (challenge-eligible package) +function check_for_steam() { + check_install_for is_steam_package "$@" +} + +function check_for_greylisted() { + check_install_for is_greylisted_package_name "$@" +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_ui.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_ui.sh new file mode 100755 index 00000000..95dcf0b5 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pw_ui.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Helpers sourced by the entry script. + +# Function to display help +function show_help() { + echo -e "${BOLD}Pacman Wrapper Help${NC}" + echo "This wrapper adds helpful features while preserving all pacman functionality." + echo "" + echo "Additional commands:" + echo " --help-wrapper Show this help message" + echo " --makepkg-capped Run makepkg in a constrained systemd user scope" + echo " (forward remaining args to makepkg)" +} + +run_makepkg_capped() { + if [[ ! -x $MAKEPKG_CAPPED_BIN ]]; then + echo -e "${RED}makepkg capped wrapper not found at ${MAKEPKG_CAPPED_BIN}${NC}" >&2 + echo -e "${YELLOW}Run install_pacman_wrapper.sh to install it.${NC}" >&2 + return 1 + fi + + if [[ $EUID -eq 0 && -n ${SUDO_USER:-} ]]; then + exec sudo -u "$SUDO_USER" "$MAKEPKG_CAPPED_BIN" "$@" + fi + + exec "$MAKEPKG_CAPPED_BIN" "$@" +} + +# Function to display a message before executing +function display_operation() { + case "$1" in + -S) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -Sy) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -S\ *) + echo -e "${BLUE}Installing packages...${NC}" >&2 + ;; + -Syu | -Syyu) + echo -e "${BLUE}Updating system...${NC}" >&2 + ;; + -R) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Rs) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Rns) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -R\ *) + echo -e "${YELLOW}Removing packages...${NC}" >&2 + ;; + -Ss) + echo -e "${CYAN}Searching for packages...${NC}" >&2 + ;; + -Ss\ *) + echo -e "${CYAN}Searching for packages...${NC}" >&2 + ;; + -Q) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Qs) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Qi) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Ql) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -Q\ *) + echo -e "${CYAN}Querying package database...${NC}" >&2 + ;; + -U) + echo -e "${BLUE}Installing local packages...${NC}" >&2 + ;; + -U\ *) + echo -e "${BLUE}Installing local packages...${NC}" >&2 + ;; + -Scc) + echo -e "${YELLOW}Cleaning package cache...${NC}" >&2 + ;; + *) + echo -e "${CYAN}Executing pacman command...${NC}" >&2 + ;; + esac +}