commit a194c14077165ad150235cf8d27257d2ae07e0e7 Author: John A. Hoeven Date: Wed Jun 10 02:25:38 2026 +0200 Add CE OS Alpine installer v0.0.1 — Tier 0/1/2 dev scaffold Staged installer set for Alpine Linux x86-64 (tinkerpad/ThinkPad T480). Covers Base (Tier 0), Minimal (Tier 1), and Basic (Tier 2) tiers with full phased structure, per-package error capture, and re-runnable test suites. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a1728f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# CE OS Alpine Installer — .gitignore + +# macOS +.DS_Store + +# Logs (generated at runtime on target machines) +*.log + +# Temp files from 11_wiki.sh glow download +/tmp/ce-glow-*/ diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..2b9c201 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# bootstrap.sh +# Served at: http://192.168.0.138/repo/scripts/alpine-ceos-installer/bootstrap.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Run this on tinkerpad to download the full CE OS installer from workbench. +# Usage: wget http://192.168.0.138/repo/scripts/alpine-ceos-installer/bootstrap.sh +# bash bootstrap.sh +# +# TODO — Forgejo: when git.jahnet.lan is deployed, replace the wget block +# below with these three lines and delete the rest of this file: +# doas apk add git +# git clone -b dev http://git.jahnet.lan/john/ceos-alpine-installer.git +# cd ceos-alpine-installer && bash ce-install.sh +# --------------------------------------------------------------------------- + +BASE="http://192.168.0.138/repo/scripts/alpine-ceos-installer" +DEST="/tmp/ceos-installer-$(date +%Y%m%d%H%M%S)" + +echo "CE OS Alpine Installer — Bootstrap" +echo "===================================" +echo "Source: ${BASE}" +echo "Dest: ${DEST}" +echo "" + +mkdir -p \ + "${DEST}/lib/base" \ + "${DEST}/lib/minimal" \ + "${DEST}/lib/basic" \ + "${DEST}/tests" + +fetch() { + local path="${1}" + if wget -q -O "${DEST}/${path}" "${BASE}/${path}"; then + echo " OK ${path}" + else + echo " ERR ${path}" >&2 + echo "Bootstrap failed: could not fetch ${path}" >&2 + exit 1 + fi +} + +echo "Fetching orchestrators..." +fetch "ce-install.sh" +fetch "ce-base.sh" +fetch "ce-minimal.sh" +fetch "ce-basic.sh" + +echo "Fetching library..." +fetch "lib/ce-common.sh" +fetch "lib/base/01_core.sh" +fetch "lib/base/02_doas.sh" + +echo "Fetching Tier 1 — Minimal..." +for n in 01_shell 02_terminal 03_files 04_vcs 05_systools \ + 06_comms 07_journal 08_web 09_network 10_record \ + 11_wiki 12_access; do + fetch "lib/minimal/${n}.sh" +done + +echo "Fetching Tier 2 — Basic..." +for n in 01_comms 02_monitor 03_gui 04_media 05_fonts 06_utils; do + fetch "lib/basic/${n}.sh" +done + +echo "Fetching tests..." +for t in test_base test_minimal test_basic; do + fetch "tests/${t}.sh" +done + +find "${DEST}" -name "*.sh" -exec chmod +x {} \; + +echo "" +echo "Bootstrap complete." +echo "" +echo "To install:" +echo " cd ${DEST}" +echo " bash ce-install.sh # full stack (Tier 0 + 1 + 2)" +echo " bash ce-base.sh # Tier 0 only" +echo " bash ce-minimal.sh # Tier 1 only (requires Tier 0)" +echo " bash ce-basic.sh # Tier 2 only (requires Tier 1)" +echo "" +echo "To run tests after install:" +echo " bash ${DEST}/tests/test_base.sh" +echo " bash ${DEST}/tests/test_minimal.sh" +echo " bash ${DEST}/tests/test_basic.sh" diff --git a/ce-base.sh b/ce-base.sh new file mode 100644 index 0000000..a11e837 --- /dev/null +++ b/ce-base.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# ce-base.sh +# ~/projects/ceos/alpine-installer/ce-base.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Tier 0 — Base. Prerequisites common to all hardware. +# Standalone-runnable. Sources lib/ce-common.sh and lib/base/*.sh. +# --------------------------------------------------------------------------- + +SCRIPT_NAME="ce-base" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${SCRIPT_DIR}/lib/ce-common.sh" \ + || { echo "ERROR: lib/ce-common.sh not found"; exit 1; } +source "${SCRIPT_DIR}/lib/base/01_core.sh" \ + || { log_error "lib/base/01_core.sh not found"; exit 1; } +source "${SCRIPT_DIR}/lib/base/02_doas.sh" \ + || { log_error "lib/base/02_doas.sh not found"; exit 1; } + +cleanup() { + local exit_code="${?}" + log_info "Running cleanup..." + doas apk cache clean >> "${LOG_FILE}" 2>&1 || true + doas rm -rf /var/cache/apk/* >> "${LOG_FILE}" 2>&1 || true + log_info "Cleanup complete (exit code: ${exit_code})" +} +trap cleanup EXIT INT TERM HUP + +phase_preflight() { + log_info "=== TIER 0: PRE-FLIGHT ===" + check_not_root + run_test "bash >= 4" '[[ "${BASH_VERSINFO[0]}" -ge 4 ]]' fail + run_test "arch detected" '[[ -n "${ARCH}" ]]' fail + run_test "apk available" 'command -v apk >/dev/null 2>&1' fail + run_test "disk >= 500MB" '[[ $(df / --output=avail | tail -1) -ge 512000 ]]' warn + run_test "internet reachable" \ + 'curl --silent --max-time 10 --output /dev/null https://dl-cdn.alpinelinux.org' warn + log_info "Arch: ${ARCH}" + log_info "Pre-flight complete" +} + +phase_install() { + log_info "=== TIER 0: INSTALL ===" + doas apk update >> "${LOG_FILE}" 2>&1 \ + && doas apk upgrade >> "${LOG_FILE}" 2>&1 \ + || { log_error "apk update/upgrade failed"; exit 1; } + run_test "apk functional post-update" \ + 'apk search bash 2>/dev/null | grep -q "^bash"' fail + install_base_core + install_base_doas +} + +phase_verify() { + log_info "=== TIER 0: VERIFY ===" + run_test "bash present" 'command -v bash >/dev/null 2>&1' warn + run_test "curl present" 'command -v curl >/dev/null 2>&1' warn + run_test "ssh present" 'command -v ssh >/dev/null 2>&1' warn + run_test "doas present" 'command -v doas >/dev/null 2>&1' warn + run_test "doas wheel.conf" '[[ -f /etc/doas.d/wheel.conf ]]' warn + run_test "community repo live" 'grep -q "^[^#].*community" /etc/apk/repositories' warn + log_info "Tier 0 verify complete" +} + +log_info "=== CE OS INSTALLER — TIER 0: BASE ===" +log_info "Arch: ${ARCH} | Log: ${LOG_FILE}" + +phase_preflight +confirm_proceed +phase_install +phase_verify +print_summary + +log_info "=== TIER 0 COMPLETE ===" diff --git a/ce-basic.sh b/ce-basic.sh new file mode 100644 index 0000000..f7ea808 --- /dev/null +++ b/ce-basic.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# ce-basic.sh +# ~/projects/ceos/alpine-installer/ce-basic.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Tier 2 — Basic. Daily-use additions on top of Minimal. +# Standalone-runnable. Sources lib/ce-common.sh and lib/basic/*.sh. +# --------------------------------------------------------------------------- + +SCRIPT_NAME="ce-basic" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${SCRIPT_DIR}/lib/ce-common.sh" \ + || { echo "ERROR: lib/ce-common.sh not found"; exit 1; } + +for _sub in "${SCRIPT_DIR}"/lib/basic/*.sh; do + source "${_sub}" || { log_error "Failed to source: ${_sub}"; exit 1; } +done +unset _sub + +cleanup() { + local exit_code="${?}" + log_info "Running cleanup..." + doas apk cache clean >> "${LOG_FILE}" 2>&1 || true + doas rm -rf /var/cache/apk/* >> "${LOG_FILE}" 2>&1 || true + log_info "Cleanup complete (exit code: ${exit_code})" +} +trap cleanup EXIT INT TERM HUP + +phase_preflight() { + log_info "=== TIER 2: PRE-FLIGHT ===" + check_not_root + run_test "bash >= 4" '[[ "${BASH_VERSINFO[0]}" -ge 4 ]]' fail + run_test "arch detected" '[[ -n "${ARCH}" ]]' fail + run_test "doas present" 'command -v doas >/dev/null 2>&1' fail + run_test "Tier 1 complete" 'command -v tmux >/dev/null 2>&1' fail + run_test "community repo enabled" 'grep -q "^[^#].*community" /etc/apk/repositories' fail + run_test "internet reachable" \ + 'curl --silent --max-time 10 --output /dev/null https://dl-cdn.alpinelinux.org' warn + run_test "disk >= 2GB" '[[ $(df / --output=avail | tail -1) -ge 2097152 ]]' warn + log_info "Arch: ${ARCH}" + log_info "Pre-flight complete" +} + +phase_install() { + log_info "=== TIER 2: INSTALL ===" + doas apk update >> "${LOG_FILE}" 2>&1 \ + && doas apk upgrade >> "${LOG_FILE}" 2>&1 \ + || { log_error "apk update/upgrade failed"; exit 1; } + run_test "apk functional" 'apk search jq 2>/dev/null | grep -q "^jq"' fail + + install_basic_comms + install_basic_monitor + install_basic_gui + install_basic_media + install_basic_fonts + install_basic_utils +} + +phase_verify() { + log_info "=== TIER 2: VERIFY ===" + local tools=(jq bc zip unzip ffmpeg mpv cmus figlet) + for tool in "${tools[@]}"; do + run_test "${tool} present" "command -v ${tool} >/dev/null 2>&1" warn + done + run_test "cagebreak present" 'command -v cagebreak >/dev/null 2>&1' warn + run_test "firefox present" 'command -v firefox >/dev/null 2>&1' warn + log_info "Tier 2 verify complete" +} + +log_info "=== CE OS INSTALLER — TIER 2: BASIC ===" +log_info "Arch: ${ARCH} | Log: ${LOG_FILE}" + +phase_preflight +confirm_proceed +phase_install +phase_verify +print_summary + +log_info "=== TIER 2 COMPLETE ===" diff --git a/ce-install.sh b/ce-install.sh new file mode 100644 index 0000000..8c2bfcc --- /dev/null +++ b/ce-install.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# ce-install.sh +# ~/projects/ceos/alpine-installer/ce-install.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Top-level orchestrator. Runs Tier 0 → 1 → 2 in sequence. +# Stops if any tier exits non-zero. One confirmation prompt for all tiers. +# --------------------------------------------------------------------------- + +SCRIPT_NAME="ce-install" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${SCRIPT_DIR}/lib/ce-common.sh" \ + || { echo "ERROR: lib/ce-common.sh not found"; exit 1; } + +check_not_root + +TIERS_COMPLETED=() +TIERS_FAILED=() + +print_final_summary() { + log_info "=== FULL INSTALL SUMMARY ===" + log_info "Completed: ${TIERS_COMPLETED[*]:-none}" + if [[ ${#TIERS_FAILED[@]} -gt 0 ]]; then + log_warn "Failed: ${TIERS_FAILED[*]}" + fi + log_info "Full log: ${LOG_FILE}" + log_info "Per-tier logs: ${LOG_DIR}/ce-base-*.log, ce-minimal-*.log, ce-basic-*.log" +} + +run_tier() { + local name="${1}" script="${2}" + log_info "--- Starting ${name} ---" + if CE_INSTALL_NO_CONFIRM=1 bash "${script}"; then + TIERS_COMPLETED+=("${name}") + log_info "--- ${name} complete ---" + else + TIERS_FAILED+=("${name}") + log_error "--- ${name} FAILED — stopping ---" + print_final_summary + exit 1 + fi +} + +log_info "=== CE OS INSTALLER — FULL STACK ===" +log_info "Arch: ${ARCH} | Log: ${LOG_FILE}" +log_info "" +log_info " Tier 0 — Base : prerequisites, SSH, doas" +log_info " Tier 1 — Minimal : full TUI workstation" +log_info " Tier 2 — Basic : daily-use additions + GUI" +log_info "" + +confirm_proceed + +run_tier "Tier 0 — Base" "${SCRIPT_DIR}/ce-base.sh" +run_tier "Tier 1 — Minimal" "${SCRIPT_DIR}/ce-minimal.sh" +run_tier "Tier 2 — Basic" "${SCRIPT_DIR}/ce-basic.sh" + +print_final_summary +log_info "=== INSTALL COMPLETE ===" diff --git a/ce-minimal.sh b/ce-minimal.sh new file mode 100644 index 0000000..932b6fd --- /dev/null +++ b/ce-minimal.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# ce-minimal.sh +# ~/projects/ceos/alpine-installer/ce-minimal.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Tier 1 — Minimal. Full TUI workstation on top of Base. +# Standalone-runnable. Sources lib/ce-common.sh and lib/minimal/*.sh. +# --------------------------------------------------------------------------- + +SCRIPT_NAME="ce-minimal" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${SCRIPT_DIR}/lib/ce-common.sh" \ + || { echo "ERROR: lib/ce-common.sh not found"; exit 1; } + +for _sub in "${SCRIPT_DIR}"/lib/minimal/*.sh; do + source "${_sub}" || { log_error "Failed to source: ${_sub}"; exit 1; } +done +unset _sub + +cleanup() { + local exit_code="${?}" + log_info "Running cleanup..." + doas apk cache clean >> "${LOG_FILE}" 2>&1 || true + doas rm -rf /var/cache/apk/* >> "${LOG_FILE}" 2>&1 || true + log_info "Cleanup complete (exit code: ${exit_code})" +} +trap cleanup EXIT INT TERM HUP + +phase_preflight() { + log_info "=== TIER 1: PRE-FLIGHT ===" + check_not_root + run_test "bash >= 4" '[[ "${BASH_VERSINFO[0]}" -ge 4 ]]' fail + run_test "arch detected" '[[ -n "${ARCH}" ]]' fail + run_test "doas present" 'command -v doas >/dev/null 2>&1' fail + run_test "Tier 0 complete" 'command -v curl >/dev/null 2>&1' fail + run_test "community repo enabled" 'grep -q "^[^#].*community" /etc/apk/repositories' fail + run_test "internet reachable" \ + 'curl --silent --max-time 10 --output /dev/null https://dl-cdn.alpinelinux.org' warn + run_test "disk >= 1GB" '[[ $(df / --output=avail | tail -1) -ge 1048576 ]]' warn + log_info "Arch: ${ARCH}" + log_info "Pre-flight complete" +} + +phase_install() { + log_info "=== TIER 1: INSTALL ===" + doas apk update >> "${LOG_FILE}" 2>&1 \ + && doas apk upgrade >> "${LOG_FILE}" 2>&1 \ + || { log_error "apk update/upgrade failed"; exit 1; } + run_test "apk functional" 'apk search vim 2>/dev/null | grep -q "^vim"' fail + + install_minimal_shell + install_minimal_terminal + install_minimal_files + install_minimal_vcs + install_minimal_systools + install_minimal_comms + install_minimal_journal + install_minimal_web + install_minimal_network + install_minimal_record + install_minimal_wiki + install_minimal_access +} + +phase_verify() { + log_info "=== TIER 1: VERIFY ===" + local tools=(tmux vim git gpg python3 lynx) + for tool in "${tools[@]}"; do + run_test "${tool} present" "command -v ${tool} >/dev/null 2>&1" warn + done + run_test "pipx present" 'command -v pipx >/dev/null 2>&1' warn + run_test "espeak-ng present" 'command -v espeak-ng >/dev/null 2>&1' warn + log_info "Tier 1 verify complete" +} + +log_info "=== CE OS INSTALLER — TIER 1: MINIMAL ===" +log_info "Arch: ${ARCH} | Log: ${LOG_FILE}" + +phase_preflight +confirm_proceed +phase_install +phase_verify +print_summary + +log_info "=== TIER 1 COMPLETE ===" diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..76400dc --- /dev/null +++ b/deploy.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# deploy.sh +# ~/projects/ceos/alpine-installer/deploy.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: workbench (Pi 5, Alpine aarch64) — run on workbench, not tinkerpad +# --------------------------------------------------------------------------- +# Copies the script tree to /srv/repo/scripts/alpine-ceos-installer/ so +# tinkerpad can fetch files via bootstrap.sh. +# +# TODO — Forgejo: when git.jahnet.lan is deployed, replace this script with: +# git push origin dev +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_TARGET="/srv/repo/scripts/alpine-ceos-installer" + +echo "CE OS Alpine Installer — Deploy to serve tree" +echo "Source: ${SCRIPT_DIR}" +echo "Target: ${DEPLOY_TARGET}" +echo "" +read -r -p "Proceed with deploy? [y/N] " confirm +[[ "${confirm,,}" == "y" ]] || { echo "Aborted."; exit 0; } + +doas mkdir -p \ + "${DEPLOY_TARGET}/lib/base" \ + "${DEPLOY_TARGET}/lib/minimal" \ + "${DEPLOY_TARGET}/lib/basic" \ + "${DEPLOY_TARGET}/tests" \ + "${DEPLOY_TARGET}/spec" + +doas cp -r "${SCRIPT_DIR}/." "${DEPLOY_TARGET}/" + +doas chmod +x \ + "${DEPLOY_TARGET}"/*.sh \ + "${DEPLOY_TARGET}"/lib/base/*.sh \ + "${DEPLOY_TARGET}"/lib/minimal/*.sh \ + "${DEPLOY_TARGET}"/lib/basic/*.sh \ + "${DEPLOY_TARGET}"/tests/*.sh + +echo "" +echo "Deploy complete." +echo "Pull URL: http://192.168.0.138/repo/scripts/alpine-ceos-installer/" +echo "Bootstrap: wget http://192.168.0.138/repo/scripts/alpine-ceos-installer/bootstrap.sh" diff --git a/lib/base/01_core.sh b/lib/base/01_core.sh new file mode 100644 index 0000000..f662817 --- /dev/null +++ b/lib/base/01_core.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 01_core.sh +# ~/projects/ceos/alpine-installer/lib/base/01_core.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Base Tier subscript: enable community repo, install core packages, +# run setup-utmp. Defines install_base_core(). +# --------------------------------------------------------------------------- + +install_base_core() { + log_info "--- base/01_core: enabling community repo ---" + if grep -q "^[^#].*community" /etc/apk/repositories 2>/dev/null; then + log_info "Community repo already enabled — skipping" + else + doas sed -i '/^#.*community/s/^#//' /etc/apk/repositories \ + >> "${LOG_FILE}" 2>&1 \ + || log_warn "Could not uncomment community repo — check /etc/apk/repositories" + run_test "community repo enabled" \ + 'grep -q "^[^#].*community" /etc/apk/repositories' warn + fi + + log_info "--- base/01_core: verifying core packages available ---" + local missing=() + for pkg in bash bash-completion ca-certificates curl openssh tzdata utmps; do + apk search "${pkg}" 2>/dev/null | grep -q "^${pkg}" \ + || missing+=("${pkg}") + done + if [[ ${#missing[@]} -gt 0 ]]; then + log_warn "Not found in repo search: ${missing[*]} — install may fail" + fi + + log_info "--- base/01_core: installing core packages ---" + local -a PKGS=(bash bash-completion ca-certificates curl openssh tzdata utmps) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + + log_info "--- base/01_core: running setup-utmp ---" + if doas setup-utmp >> "${LOG_FILE}" 2>&1; then + log_info "setup-utmp complete" + else + log_warn "setup-utmp returned non-zero — may already be configured" + fi +} + +# Standalone entry point +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="01_core" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_base_core + print_summary +fi diff --git a/lib/base/02_doas.sh b/lib/base/02_doas.sh new file mode 100644 index 0000000..e79f4d6 --- /dev/null +++ b/lib/base/02_doas.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 02_doas.sh +# ~/projects/ceos/alpine-installer/lib/base/02_doas.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Base Tier subscript: install doas and write /etc/doas.d/wheel.conf. +# Defines install_base_doas(). +# --------------------------------------------------------------------------- + +install_base_doas() { + log_info "--- base/02_doas: installing doas ---" + install_pkg "doas" + + run_test "doas binary present" 'command -v doas >/dev/null 2>&1' warn + + log_info "--- base/02_doas: configuring /etc/doas.d/wheel.conf ---" + + if ! doas test -d /etc/doas.d 2>/dev/null; then + doas mkdir -p /etc/doas.d >> "${LOG_FILE}" 2>&1 \ + || { log_warn "Could not create /etc/doas.d — skipping wheel.conf"; return 1; } + fi + + if doas test -f /etc/doas.d/wheel.conf 2>/dev/null; then + log_info "/etc/doas.d/wheel.conf already present — skipping write" + else + printf 'permit persist :wheel\n' \ + | doas tee /etc/doas.d/wheel.conf > /dev/null 2>&1 \ + || log_warn "Failed to write wheel.conf — configure manually: permit persist :wheel" + run_test "wheel.conf written" '[[ -s /etc/doas.d/wheel.conf ]]' warn + fi + + log_info "doas configuration complete" +} + +# Standalone entry point +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="02_doas" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_base_doas + print_summary +fi diff --git a/lib/basic/01_comms.sh b/lib/basic/01_comms.sh new file mode 100644 index 0000000..81722e0 --- /dev/null +++ b/lib/basic/01_comms.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 01_comms.sh +# ~/projects/ceos/alpine-installer/lib/basic/01_comms.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Basic Tier subscript: communication additions — abook, task (taskwarrior). +# Defines install_basic_comms(). +# --------------------------------------------------------------------------- + +install_basic_comms() { + log_info "--- basic/01_comms: communication additions ---" + local -a PKGS=(abook task) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "abook present" 'command -v abook >/dev/null 2>&1' warn + run_test "task present" 'command -v task >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="basic_01_comms" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_basic_comms + print_summary +fi diff --git a/lib/basic/02_monitor.sh b/lib/basic/02_monitor.sh new file mode 100644 index 0000000..d6642c5 --- /dev/null +++ b/lib/basic/02_monitor.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 02_monitor.sh +# ~/projects/ceos/alpine-installer/lib/basic/02_monitor.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Basic Tier subscript: system monitoring — nethogs, iproute2, lnav (@edge). +# lnav is an edge package; failure is logged and marked for manual follow-up. +# Defines install_basic_monitor(). +# --------------------------------------------------------------------------- + +install_basic_monitor() { + log_info "--- basic/02_monitor: system monitoring ---" + + local -a PKGS=(nethogs iproute2) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + + run_test "ip present" 'command -v ip >/dev/null 2>&1' warn + + # lnav — edge package, warn-on-fail + ensure_edge_repo + log_info "Installing lnav from @edge..." + if install_pkg_edge "lnav"; then + run_test "lnav present" 'command -v lnav >/dev/null 2>&1' warn + else + MANUAL_PKGS+=("lnav: edge install failed — requires manual verification") + fi +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="basic_02_monitor" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_basic_monitor + print_summary +fi diff --git a/lib/basic/03_gui.sh b/lib/basic/03_gui.sh new file mode 100644 index 0000000..0e7b4db --- /dev/null +++ b/lib/basic/03_gui.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 03_gui.sh +# ~/projects/ceos/alpine-installer/lib/basic/03_gui.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Basic Tier subscript: GUI layer — cagebreak, firefox, xwayland. +# Supported on x86-64. Boot config modification (/boot/cmdline.txt) is +# RPi-only and is explicitly skipped on x86-64 hardware. +# Defines install_basic_gui(). +# --------------------------------------------------------------------------- + +install_basic_gui() { + log_info "--- basic/03_gui: GUI layer (Wayland + Firefox) ---" + log_info "Arch: ${ARCH} — GUI supported on x86-64" + + local -a PKGS=(cagebreak firefox xwayland) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + + run_test "cagebreak present" 'command -v cagebreak >/dev/null 2>&1' warn + run_test "firefox present" 'command -v firefox >/dev/null 2>&1' warn + + # /boot/cmdline.txt and /boot/extlinux.conf modifications are RPi-only + log_info "NOTICE: /boot/cmdline.txt modification skipped — not applicable on x86-64 (ThinkPad)" +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="basic_03_gui" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_basic_gui + print_summary +fi diff --git a/lib/basic/04_media.sh b/lib/basic/04_media.sh new file mode 100644 index 0000000..ba2fd49 --- /dev/null +++ b/lib/basic/04_media.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 04_media.sh +# ~/projects/ceos/alpine-installer/lib/basic/04_media.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Basic Tier subscript: media — cmus, mpv, ffmpeg, imagemagick, fbida-fbi. +# Note: package is 'fbida-fbi' (not 'fbida'). +# Defines install_basic_media(). +# --------------------------------------------------------------------------- + +install_basic_media() { + log_info "--- basic/04_media: media tools ---" + local -a PKGS=(cmus mpv ffmpeg imagemagick fbida-fbi) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "cmus present" 'command -v cmus >/dev/null 2>&1' warn + run_test "mpv present" 'command -v mpv >/dev/null 2>&1' warn + run_test "ffmpeg present" 'command -v ffmpeg >/dev/null 2>&1' warn + run_test "convert present" 'command -v convert >/dev/null 2>&1' warn + run_test "fbi present" 'command -v fbi >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="basic_04_media" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_basic_media + print_summary +fi diff --git a/lib/basic/05_fonts.sh b/lib/basic/05_fonts.sh new file mode 100644 index 0000000..bcd7103 --- /dev/null +++ b/lib/basic/05_fonts.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 05_fonts.sh +# ~/projects/ceos/alpine-installer/lib/basic/05_fonts.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Basic Tier subscript: fonts and display — figlet, nerd-fonts (@edge). +# nerd-fonts package name confirmed via pkgs.alpinelinux.org (edge/community). +# Installs with @edge tag; gracefully skips on failure. +# Defines install_basic_fonts(). +# --------------------------------------------------------------------------- + +install_basic_fonts() { + log_info "--- basic/05_fonts: fonts and display ---" + + install_pkg "figlet" + run_test "figlet present" 'command -v figlet >/dev/null 2>&1' warn + + # nerd-fonts — edge/community; package name verified as 'nerd-fonts' + ensure_edge_repo + + log_info "Installing nerd-fonts from @edge/community..." + if install_pkg_edge "nerd-fonts"; then + log_info "nerd-fonts installed" + else + log_warn "nerd-fonts@edge not available — skipping gracefully" + MANUAL_PKGS+=("nerd-fonts: edge install failed — check pkgs.alpinelinux.org/packages?name=nerd-fonts&branch=edge") + fi +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="basic_05_fonts" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_basic_fonts + print_summary +fi diff --git a/lib/basic/06_utils.sh b/lib/basic/06_utils.sh new file mode 100644 index 0000000..02af323 --- /dev/null +++ b/lib/basic/06_utils.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 06_utils.sh +# ~/projects/ceos/alpine-installer/lib/basic/06_utils.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Basic Tier subscript: additional utilities — +# tealdeer, restic, jq, bc, zip, unzip, xz. +# Note: package name is 'tealdeer' (not 'tldr'). +# Defines install_basic_utils(). +# --------------------------------------------------------------------------- + +install_basic_utils() { + log_info "--- basic/06_utils: additional utilities ---" + local -a PKGS=(tealdeer restic jq bc zip unzip xz) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "tldr present" 'command -v tldr >/dev/null 2>&1' warn + run_test "restic present" 'command -v restic >/dev/null 2>&1' warn + run_test "jq present" 'command -v jq >/dev/null 2>&1' warn + run_test "bc present" 'command -v bc >/dev/null 2>&1' warn + run_test "zip present" 'command -v zip >/dev/null 2>&1' warn + run_test "xz present" 'command -v xz >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="basic_06_utils" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_basic_utils + print_summary +fi diff --git a/lib/ce-common.sh b/lib/ce-common.sh new file mode 100644 index 0000000..53d450b --- /dev/null +++ b/lib/ce-common.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# ce-common.sh +# ~/projects/ceos/alpine-installer/lib/ce-common.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Shared library: logging, run_test, package helpers, common guards. +# Source this file; do not execute directly. +# --------------------------------------------------------------------------- + +# Guard against double-sourcing +[[ -n "${CE_COMMON_LOADED:-}" ]] && return 0 +CE_COMMON_LOADED=1 + +# Architecture detection +ARCH="$(uname -m)" + +# Logging setup — SCRIPT_NAME must be set before sourcing +LOG_DIR="${HOME}/.local/logs/ceos_installer" +SCRIPT_NAME="${SCRIPT_NAME:-ce-common}" +LOG_FILE="${LOG_DIR}/${SCRIPT_NAME}-$(date +%Y-%m-%d).log" + +# Failed packages tracker — shared across all sourced subscripts +FAILED_PKGS=() + +# Packages requiring manual follow-up (distinct from simple failures) +MANUAL_PKGS=() + +mkdir -p "${LOG_DIR}" + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + +_log() { + local level="${1}" msg="${2}" ts + ts="$(date '+%Y-%m-%d %H:%M:%S')" + printf '[%s] [%s] %s\n' "${ts}" "${level}" "${msg}" | tee -a "${LOG_FILE}" +} + +log_info() { _log "INFO " "${1}"; } +log_warn() { _log "WARN " "${1}" >&2; } +log_error() { _log "ERROR" "${1}" >&2; } +log_debug() { [[ "${CE_DEBUG:-0}" == "1" ]] && _log "DEBUG" "${1}"; } + +# --------------------------------------------------------------------------- +# run_test +# --------------------------------------------------------------------------- + +run_test() { + local desc="${1}" cmd="${2}" mode="${3:-fail}" + + if eval "${cmd}" >/dev/null 2>&1; then + log_info " PASS: ${desc}" + return 0 + fi + + if [[ "${mode}" == "fail" ]]; then + log_error " FAIL: ${desc}" + exit 1 + else + log_warn " WARN: ${desc}" + return 1 + fi +} + +# --------------------------------------------------------------------------- +# Package helpers +# --------------------------------------------------------------------------- + +# install_pkg — one package, logs result +install_pkg() { + local pkg="${1}" + if doas apk add "${pkg}" >> "${LOG_FILE}" 2>&1; then + log_info "Installed: ${pkg}" + return 0 + else + log_warn "FAILED: ${pkg} — logged, continuing" + FAILED_PKGS+=("${pkg}") + return 1 + fi +} + +# install_pkg_edge — install with @edge tag, warn on fail +install_pkg_edge() { + local pkg="${1}" + if doas apk add "${pkg}@edge" >> "${LOG_FILE}" 2>&1; then + log_info "Installed: ${pkg} (@edge)" + return 0 + else + log_warn "FAILED: ${pkg}@edge — manual verification required" + FAILED_PKGS+=("${pkg} (edge, manual verification required)") + return 1 + fi +} + +# ensure_edge_repo — add @edge community tag if not present +ensure_edge_repo() { + if ! grep -q "^@edge" /etc/apk/repositories 2>/dev/null; then + log_info "Adding @edge community tag to /etc/apk/repositories" + printf '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community\n' \ + | doas tee -a /etc/apk/repositories > /dev/null 2>&1 \ + || log_warn "Could not add @edge tag — edge installs may fail" + fi +} + +# --------------------------------------------------------------------------- +# Guards and prompts +# --------------------------------------------------------------------------- + +# check_not_root — exit if accidentally run as root +check_not_root() { + if [[ "${EUID}" -eq 0 ]]; then + echo "ERROR: Do not run as root. Run as a standard user with doas configured." >&2 + exit 1 + fi +} + +# confirm_proceed — single prompt; bypassed when CE_INSTALL_NO_CONFIRM=1 +confirm_proceed() { + [[ "${CE_INSTALL_NO_CONFIRM:-0}" == "1" ]] && return 0 + echo "" + read -r -p "Proceed with installation? [y/N] " confirm + [[ "${confirm,,}" == "y" ]] || { log_info "Aborted by user."; exit 0; } +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +print_summary() { + log_info "=== INSTALLATION SUMMARY ===" + if [[ ${#FAILED_PKGS[@]} -eq 0 && ${#MANUAL_PKGS[@]} -eq 0 ]]; then + log_info "All packages installed successfully." + return 0 + fi + if [[ ${#FAILED_PKGS[@]} -gt 0 ]]; then + log_warn "Failed or skipped packages:" + for pkg in "${FAILED_PKGS[@]}"; do + log_warn " - ${pkg}" + done + fi + if [[ ${#MANUAL_PKGS[@]} -gt 0 ]]; then + log_warn "Requires manual follow-up:" + for item in "${MANUAL_PKGS[@]}"; do + log_warn " - ${item}" + done + fi +} diff --git a/lib/minimal/01_shell.sh b/lib/minimal/01_shell.sh new file mode 100644 index 0000000..73f4fe4 --- /dev/null +++ b/lib/minimal/01_shell.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 01_shell.sh +# ~/projects/ceos/alpine-installer/lib/minimal/01_shell.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: shell environment — starship, less, mandoc, ncurses. +# Defines install_minimal_shell(). +# --------------------------------------------------------------------------- + +install_minimal_shell() { + log_info "--- minimal/01_shell: shell environment ---" + local -a PKGS=(starship less mandoc ncurses) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "less present" 'command -v less >/dev/null 2>&1' warn + run_test "starship present" 'command -v starship >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="01_shell" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_shell + print_summary +fi diff --git a/lib/minimal/02_terminal.sh b/lib/minimal/02_terminal.sh new file mode 100644 index 0000000..5c167bf --- /dev/null +++ b/lib/minimal/02_terminal.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 02_terminal.sh +# ~/projects/ceos/alpine-installer/lib/minimal/02_terminal.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: terminal environment — tmux, vim, nano. +# Defines install_minimal_terminal(). +# --------------------------------------------------------------------------- + +install_minimal_terminal() { + log_info "--- minimal/02_terminal: terminal environment ---" + local -a PKGS=(tmux vim nano) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "tmux present" 'command -v tmux >/dev/null 2>&1' warn + run_test "vim present" 'command -v vim >/dev/null 2>&1' warn + run_test "nano present" 'command -v nano >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="02_terminal" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_terminal + print_summary +fi diff --git a/lib/minimal/03_files.sh b/lib/minimal/03_files.sh new file mode 100644 index 0000000..009a247 --- /dev/null +++ b/lib/minimal/03_files.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 03_files.sh +# ~/projects/ceos/alpine-installer/lib/minimal/03_files.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: file management — ranger, mc, ncdu, fzf, zoxide, stow. +# Defines install_minimal_files(). +# --------------------------------------------------------------------------- + +install_minimal_files() { + log_info "--- minimal/03_files: file management ---" + local -a PKGS=(ranger mc ncdu fzf zoxide stow) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "ranger present" 'command -v ranger >/dev/null 2>&1' warn + run_test "fzf present" 'command -v fzf >/dev/null 2>&1' warn + run_test "zoxide present" 'command -v zoxide >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="03_files" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_files + print_summary +fi diff --git a/lib/minimal/04_vcs.sh b/lib/minimal/04_vcs.sh new file mode 100644 index 0000000..a9dabfa --- /dev/null +++ b/lib/minimal/04_vcs.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 04_vcs.sh +# ~/projects/ceos/alpine-installer/lib/minimal/04_vcs.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: version control and transfer — git, rsync, openssl, gnupg. +# Defines install_minimal_vcs(). +# --------------------------------------------------------------------------- + +install_minimal_vcs() { + log_info "--- minimal/04_vcs: version control and transfer ---" + local -a PKGS=(git rsync openssl gnupg) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "git present" 'command -v git >/dev/null 2>&1' warn + run_test "rsync present" 'command -v rsync >/dev/null 2>&1' warn + run_test "gpg present" 'command -v gpg >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="04_vcs" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_vcs + print_summary +fi diff --git a/lib/minimal/05_systools.sh b/lib/minimal/05_systools.sh new file mode 100644 index 0000000..9042395 --- /dev/null +++ b/lib/minimal/05_systools.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 05_systools.sh +# ~/projects/ceos/alpine-installer/lib/minimal/05_systools.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: system tools — btop, lsof, mosh, dialog. +# Defines install_minimal_systools(). +# --------------------------------------------------------------------------- + +install_minimal_systools() { + log_info "--- minimal/05_systools: system tools ---" + local -a PKGS=(btop lsof mosh dialog) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "btop present" 'command -v btop >/dev/null 2>&1' warn + run_test "lsof present" 'command -v lsof >/dev/null 2>&1' warn + run_test "dialog present" 'command -v dialog >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="05_systools" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_systools + print_summary +fi diff --git a/lib/minimal/06_comms.sh b/lib/minimal/06_comms.sh new file mode 100644 index 0000000..a5b6767 --- /dev/null +++ b/lib/minimal/06_comms.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 06_comms.sh +# ~/projects/ceos/alpine-installer/lib/minimal/06_comms.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: communication and organisation — +# neomutt, msmtp, isync (mbsync), calcurse, pass. +# Defines install_minimal_comms(). +# --------------------------------------------------------------------------- + +install_minimal_comms() { + log_info "--- minimal/06_comms: communication and organisation ---" + local -a PKGS=(neomutt msmtp isync calcurse pass) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "neomutt present" 'command -v neomutt >/dev/null 2>&1' warn + run_test "msmtp present" 'command -v msmtp >/dev/null 2>&1' warn + run_test "mbsync present" 'command -v mbsync >/dev/null 2>&1' warn + run_test "calcurse present" 'command -v calcurse >/dev/null 2>&1' warn + run_test "pass present" 'command -v pass >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="06_comms" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_comms + print_summary +fi diff --git a/lib/minimal/07_journal.sh b/lib/minimal/07_journal.sh new file mode 100644 index 0000000..8c25582 --- /dev/null +++ b/lib/minimal/07_journal.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 07_journal.sh +# ~/projects/ceos/alpine-installer/lib/minimal/07_journal.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: journal — python3, pipx (apk), jrnl (pipx install). +# Defines install_minimal_journal(). +# --------------------------------------------------------------------------- + +install_minimal_journal() { + log_info "--- minimal/07_journal: python3, pipx, jrnl ---" + + install_pkg "python3" + install_pkg "pipx" + + run_test "python3 present" 'command -v python3 >/dev/null 2>&1' warn + run_test "pipx present" 'command -v pipx >/dev/null 2>&1' warn + + if ! command -v pipx >/dev/null 2>&1; then + log_warn "pipx not available — skipping jrnl install" + FAILED_PKGS+=("jrnl (skipped: pipx not available)") + return 0 + fi + + log_info "Installing jrnl via pipx..." + if pipx install jrnl >> "${LOG_FILE}" 2>&1; then + log_info "Installed: jrnl (via pipx)" + else + log_warn "FAILED: jrnl (pipx install) — logged, continuing" + FAILED_PKGS+=("jrnl (pipx install failed)") + fi + + run_test "jrnl present" 'command -v jrnl >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="07_journal" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_journal + print_summary +fi diff --git a/lib/minimal/08_web.sh b/lib/minimal/08_web.sh new file mode 100644 index 0000000..09866ef --- /dev/null +++ b/lib/minimal/08_web.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 08_web.sh +# ~/projects/ceos/alpine-installer/lib/minimal/08_web.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: web clients — lynx, w3m. +# Defines install_minimal_web(). +# --------------------------------------------------------------------------- + +install_minimal_web() { + log_info "--- minimal/08_web: web clients ---" + local -a PKGS=(lynx w3m) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "lynx present" 'command -v lynx >/dev/null 2>&1' warn + run_test "w3m present" 'command -v w3m >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="08_web" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_web + print_summary +fi diff --git a/lib/minimal/09_network.sh b/lib/minimal/09_network.sh new file mode 100644 index 0000000..cc53bc1 --- /dev/null +++ b/lib/minimal/09_network.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 09_network.sh +# ~/projects/ceos/alpine-installer/lib/minimal/09_network.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: network services — dbus, avahi. +# Enables both services in the default OpenRC runlevel. +# Defines install_minimal_network(). +# --------------------------------------------------------------------------- + +install_minimal_network() { + log_info "--- minimal/09_network: dbus and avahi ---" + + install_pkg "dbus" + install_pkg "avahi" + + run_test "dbus present" 'command -v dbus-daemon >/dev/null 2>&1' warn + run_test "avahi-daemon present" 'command -v avahi-daemon >/dev/null 2>&1' warn + + log_info "Enabling dbus and avahi-daemon in OpenRC default runlevel..." + + if command -v rc-update >/dev/null 2>&1; then + doas rc-update add dbus default >> "${LOG_FILE}" 2>&1 \ + && log_info "rc-update: dbus added to default" \ + || log_warn "rc-update add dbus failed — enable manually" + doas rc-update add avahi-daemon default >> "${LOG_FILE}" 2>&1 \ + && log_info "rc-update: avahi-daemon added to default" \ + || log_warn "rc-update add avahi-daemon failed — enable manually" + else + log_warn "rc-update not found — skipping service enablement" + MANUAL_PKGS+=("dbus + avahi-daemon: enable with rc-update add default") + fi +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="09_network" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_network + print_summary +fi diff --git a/lib/minimal/10_record.sh b/lib/minimal/10_record.sh new file mode 100644 index 0000000..5ba04fd --- /dev/null +++ b/lib/minimal/10_record.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 10_record.sh +# ~/projects/ceos/alpine-installer/lib/minimal/10_record.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: recording and screenshot — asciinema, fbgrab. +# Defines install_minimal_record(). +# --------------------------------------------------------------------------- + +install_minimal_record() { + log_info "--- minimal/10_record: recording and screenshot ---" + local -a PKGS=(asciinema fbgrab) + for pkg in "${PKGS[@]}"; do + install_pkg "${pkg}" + done + run_test "asciinema present" 'command -v asciinema >/dev/null 2>&1' warn + run_test "fbgrab present" 'command -v fbgrab >/dev/null 2>&1' warn +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="10_record" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_record + print_summary +fi diff --git a/lib/minimal/11_wiki.sh b/lib/minimal/11_wiki.sh new file mode 100644 index 0000000..38a5a02 --- /dev/null +++ b/lib/minimal/11_wiki.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 11_wiki.sh +# ~/projects/ceos/alpine-installer/lib/minimal/11_wiki.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: wiki viewer — glow from CE repo. +# Downloads binary, verifies sha256 and GPG signature, installs to ~/.local/bin/. +# Gracefully skips if CE repo is unreachable or verification fails. +# Defines install_minimal_wiki(). +# --------------------------------------------------------------------------- + +install_minimal_wiki() { + log_info "--- minimal/11_wiki: glow (CE repo) ---" + + local CE_REPO="https://cervelloelettrico.it/bin/amd64/alpine" + local INSTALL_DIR="${HOME}/.local/bin" + local TEMP_DIR + TEMP_DIR="$(mktemp -d /tmp/ce-glow-XXXXXX)" + + mkdir -p "${INSTALL_DIR}" + + # Abort cleanly if CE repo unreachable + if ! curl --silent --max-time 10 --output /dev/null "${CE_REPO}/glow"; then + log_warn "CE repo unreachable (${CE_REPO}) — skipping glow" + FAILED_PKGS+=("glow (CE repo unreachable)") + rm -rf "${TEMP_DIR}" + return 0 + fi + + log_info "Downloading glow, sha256, and sig from CE repo..." + local ok=1 + curl --silent --max-time 30 -o "${TEMP_DIR}/glow" "${CE_REPO}/glow" || ok=0 + curl --silent --max-time 10 -o "${TEMP_DIR}/glow.sha256" "${CE_REPO}/glow.sha256" || ok=0 + curl --silent --max-time 10 -o "${TEMP_DIR}/glow.sig" "${CE_REPO}/glow.sig" || ok=0 + + if [[ "${ok}" -eq 0 ]]; then + log_warn "glow download incomplete — skipping install" + FAILED_PKGS+=("glow (download failed)") + rm -rf "${TEMP_DIR}" + return 0 + fi + + # SHA256 verification + local expected actual + expected="$(awk '{print $1}' "${TEMP_DIR}/glow.sha256")" + actual="$(sha256sum "${TEMP_DIR}/glow" | awk '{print $1}')" + if [[ "${expected}" != "${actual}" ]]; then + log_warn "glow SHA256 mismatch — aborting (expected: ${expected}, got: ${actual})" + FAILED_PKGS+=("glow (sha256 mismatch)") + rm -rf "${TEMP_DIR}" + return 0 + fi + log_info "glow SHA256 verified" + + # GPG signature verification (gnupg available from 04_vcs) + if command -v gpg >/dev/null 2>&1; then + if gpg --verify "${TEMP_DIR}/glow.sig" "${TEMP_DIR}/glow" >> "${LOG_FILE}" 2>&1; then + log_info "glow GPG signature verified" + else + log_warn "glow GPG verification failed — aborting install" + FAILED_PKGS+=("glow (gpg verify failed)") + rm -rf "${TEMP_DIR}" + return 0 + fi + else + log_warn "gpg not available — proceeding with sha256-only verification" + fi + + chmod +x "${TEMP_DIR}/glow" + cp "${TEMP_DIR}/glow" "${INSTALL_DIR}/glow" + rm -rf "${TEMP_DIR}" + + run_test "glow installed" 'command -v glow >/dev/null 2>&1' warn + log_info "glow installed to ${INSTALL_DIR}/glow" +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="11_wiki" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_wiki + print_summary +fi diff --git a/lib/minimal/12_access.sh b/lib/minimal/12_access.sh new file mode 100644 index 0000000..b010edd --- /dev/null +++ b/lib/minimal/12_access.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# 12_access.sh +# ~/projects/ceos/alpine-installer/lib/minimal/12_access.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Minimal Tier subscript: accessibility — espeak-ng, espeakup, speech-dispatcher. +# espeak conflict trap: removes espeak before installing espeak-ng. +# espeakup: repo-verified before install (package availability varies by release). +# speech-dispatcher: @edge, warn-on-fail. +# ly login manager: aarch64 CE repo only — SKIPPED on x86-64 with notice. +# Defines install_minimal_access(). +# --------------------------------------------------------------------------- + +install_minimal_access() { + log_info "--- minimal/12_access: accessibility ---" + + # espeak conflict trap — must run before espeak-ng install + if doas apk info -e espeak >> "${LOG_FILE}" 2>&1; then + log_info "Removing conflicting 'espeak' package before installing espeak-ng..." + doas apk del espeak >> "${LOG_FILE}" 2>&1 \ + || log_warn "Could not remove espeak — espeak-ng may conflict" + fi + + install_pkg "espeak-ng" + run_test "espeak-ng present" 'command -v espeak-ng >/dev/null 2>&1' warn + + # espeakup — verify package exists in current repo before attempting install + if apk search espeakup 2>/dev/null | grep -q "^espeakup"; then + install_pkg "espeakup" + else + log_warn "espeakup not found in current Alpine repos — skipping" + MANUAL_PKGS+=("espeakup: not found in Alpine v3.23 — check https://github.com/dermotbradley/alpine-espeakup") + fi + + # speech-dispatcher — edge package + ensure_edge_repo + install_pkg_edge "speech-dispatcher" + + # ly login manager — CE repo, aarch64 only — skip on x86-64 + if [[ "${ARCH}" == "x86_64" ]]; then + log_info "NOTICE: ly login manager — CE repo aarch64 only, not available for x86-64 — skipped" + MANUAL_PKGS+=("ly: aarch64 CE repo only — not available for x86-64 (${ARCH})") + fi +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + SCRIPT_NAME="12_access" + source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + check_not_root + install_minimal_access + print_summary +fi diff --git a/spec/install_spec.md b/spec/install_spec.md new file mode 100644 index 0000000..5445163 --- /dev/null +++ b/spec/install_spec.md @@ -0,0 +1,132 @@ +# CE OS Alpine Installer — Subscript Specifications + + + +--- + +## lib/ce-common.sh + +| Field | Value | +|---|---| +| Name | ce-common.sh — shared library | +| Inputs | SCRIPT_NAME env var (set by caller before sourcing) | +| Outputs | LOG_FILE written, FAILED_PKGS/MANUAL_PKGS arrays populated | +| Error behaviour | run_test exits on fail mode; all others log-and-continue | +| Privilege | standard user (doas called internally by install_pkg) | +| Distro target | Alpine | + +--- + +## lib/base/01_core.sh + +| Field | Value | +|---|---| +| Name | 01_core.sh — enable community repo, core packages, setup-utmp | +| Inputs | none (sources ce-common.sh) | +| Outputs | packages installed; /etc/apk/repositories modified | +| Error behaviour | per-package log-and-continue; setup-utmp warn-only | +| Privilege | requires doas for apk and sed on /etc/apk/repositories | +| Distro target | Alpine | + +## lib/base/02_doas.sh + +| Field | Value | +|---|---| +| Name | 02_doas.sh — doas install and wheel.conf | +| Inputs | none | +| Outputs | doas installed; /etc/doas.d/wheel.conf written | +| Error behaviour | warn if wheel.conf write fails — manual action noted | +| Privilege | requires doas for mkdir/tee on /etc/doas.d | +| Distro target | Alpine | + +--- + +## lib/minimal/01_shell.sh — 06_comms.sh + +Standard pattern: apk community packages, per-package install_pkg, run_test warn. + +| Script | Packages | +|---|---| +| 01_shell.sh | starship, less, mandoc, ncurses | +| 02_terminal.sh | tmux, vim, nano | +| 03_files.sh | ranger, mc, ncdu, fzf, zoxide, stow | +| 04_vcs.sh | git, rsync, openssl, gnupg | +| 05_systools.sh | btop, lsof, mosh, dialog | +| 06_comms.sh | neomutt, msmtp, isync, calcurse, pass | + +--- + +## lib/minimal/07_journal.sh + +| Field | Value | +|---|---| +| Name | 07_journal.sh — python3, pipx, jrnl | +| Inputs | none | +| Outputs | python3 + pipx via apk; jrnl via pipx install | +| Error behaviour | skips jrnl if pipx unavailable; logs failure | +| Privilege | standard user (pipx runs as user) | +| Distro target | Alpine | + +## lib/minimal/08_web.sh + +Standard pattern: lynx, w3m. + +## lib/minimal/09_network.sh + +| Field | Value | +|---|---| +| Name | 09_network.sh — dbus, avahi + rc-update | +| Inputs | none | +| Outputs | dbus + avahi installed; both added to default runlevel | +| Error behaviour | rc-update failures warn-only; logged to MANUAL_PKGS | +| Privilege | doas for apk + rc-update | +| Distro target | Alpine + OpenRC | + +## lib/minimal/10_record.sh + +Standard pattern: asciinema, fbgrab. + +## lib/minimal/11_wiki.sh + +| Field | Value | +|---|---| +| Name | 11_wiki.sh — glow binary from CE repo | +| Inputs | CE_REPO URL (hardcoded to cervelloelettrico.it/bin/amd64/alpine) | +| Outputs | glow installed to ~/.local/bin/glow | +| Error behaviour | graceful skip on unreachable repo, download fail, sha256 mismatch, gpg fail | +| Privilege | standard user (installs to ~/.local/bin) | +| Distro target | Alpine x86-64; CE_REPO path must change for other arches | +| Doc references | cervelloelettrico.it CE repo | + +## lib/minimal/12_access.sh + +| Field | Value | +|---|---| +| Name | 12_access.sh — espeak-ng, espeakup, speech-dispatcher, ly notice | +| Inputs | ARCH (from ce-common.sh) | +| Outputs | espeak removed if present; espeak-ng installed; espeakup if available; speech-dispatcher@edge; ly notice on x86-64 | +| Error behaviour | espeak removal warn-only; espeakup skipped if not in repo; speech-dispatcher warn-on-fail; ly skipped with MANUAL notice | +| Privilege | doas for apk del + apk add | +| Distro target | Alpine; espeakup availability varies by release | +| Doc references | wiki.alpinelinux.org/wiki/Using_espeak_on_Alpine_Linux; github.com/dermotbradley/alpine-espeakup | + +--- + +## lib/basic/01_comms.sh — 06_utils.sh + +| Script | Packages | Notes | +|---|---|---| +| 01_comms.sh | abook, task | task = taskwarrior (package name is 'task') | +| 02_monitor.sh | nethogs, iproute2, lnav@edge | lnav is edge; warn-on-fail + MANUAL note | +| 03_gui.sh | cagebreak, firefox, xwayland | /boot/cmdline.txt skip noted; x86-64 only meaningful | +| 04_media.sh | cmus, mpv, ffmpeg, imagemagick, fbida-fbi | package is 'fbida-fbi' not 'fbida' | +| 05_fonts.sh | figlet, nerd-fonts@edge | nerd-fonts confirmed package name (edge/community) | +| 06_utils.sh | tealdeer, restic, jq, bc, zip, unzip, xz | tealdeer (not 'tldr') | + +--- + +Source: ce-authored +Source-type: ce-authored +Licence: Public Domain — CE FFM Policy +Verified-on: 2026-06-10 +Session: ceos-alpine-installer-v0.0.1-dev diff --git a/tests/test_base.sh b/tests/test_base.sh new file mode 100644 index 0000000..e7135d6 --- /dev/null +++ b/tests/test_base.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# test_base.sh +# ~/projects/ceos/alpine-installer/tests/test_base.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Re-runnable test suite for Tier 0 (Base). Run after ce-base.sh. +# Exits with count of failures (0 = all passed). +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT_NAME="test_base" +source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + +PASS=0 +FAIL=0 + +check() { + local desc="${1}" cmd="${2}" + if eval "${cmd}" >/dev/null 2>&1; then + log_info "PASS: ${desc}" + ((PASS++)) + else + log_warn "FAIL: ${desc}" + ((FAIL++)) + fi +} + +log_info "=== TEST SUITE: TIER 0 BASE ===" + +check "bash present" 'command -v bash' +check "bash-completion installed" 'apk info -e bash-completion' +check "ca-certificates installed" 'apk info -e ca-certificates' +check "curl present" 'command -v curl' +check "openssh installed" 'apk info -e openssh' +check "sshd_config present" '[[ -f /etc/ssh/sshd_config ]]' +check "tzdata installed" 'apk info -e tzdata' +check "utmps installed" 'apk info -e utmps' +check "doas present" 'command -v doas' +check "doas wheel.conf present" '[[ -f /etc/doas.d/wheel.conf ]]' +check "wheel.conf non-empty" '[[ -s /etc/doas.d/wheel.conf ]]' +check "community repo enabled" 'grep -q "^[^#].*community" /etc/apk/repositories' + +log_info "=== RESULTS: ${PASS} passed, ${FAIL} failed ===" +[[ "${FAIL}" -eq 0 ]] \ + && log_info "All Tier 0 tests passed." \ + || log_warn "${FAIL} test(s) failed — review ${LOG_FILE}" +exit "${FAIL}" diff --git a/tests/test_basic.sh b/tests/test_basic.sh new file mode 100644 index 0000000..7b3044c --- /dev/null +++ b/tests/test_basic.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# test_basic.sh +# ~/projects/ceos/alpine-installer/tests/test_basic.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Re-runnable test suite for Tier 2 (Basic). Run after ce-basic.sh. +# Exits with count of failures (0 = all passed). +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT_NAME="test_basic" +source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + +PASS=0 +FAIL=0 + +check() { + local desc="${1}" cmd="${2}" + if eval "${cmd}" >/dev/null 2>&1; then + log_info "PASS: ${desc}" + ((PASS++)) + else + log_warn "FAIL: ${desc}" + ((FAIL++)) + fi +} + +log_info "=== TEST SUITE: TIER 2 BASIC ===" + +# Comms additions +check "abook present" 'command -v abook' +check "task present" 'command -v task' + +# Monitoring +check "nethogs present" 'command -v nethogs' +check "ip present" 'command -v ip' + +# GUI +check "cagebreak present" 'command -v cagebreak' +check "firefox present" 'command -v firefox' +check "xwayland present" 'command -v Xwayland' + +# Media +check "cmus present" 'command -v cmus' +check "mpv present" 'command -v mpv' +check "ffmpeg present" 'command -v ffmpeg' +check "convert present" 'command -v convert' +check "fbi present" 'command -v fbi' + +# Fonts +check "figlet present" 'command -v figlet' + +# Utilities +check "tldr present" 'command -v tldr' +check "restic present" 'command -v restic' +check "jq present" 'command -v jq' +check "bc present" 'command -v bc' +check "zip present" 'command -v zip' +check "unzip present" 'command -v unzip' +check "xz present" 'command -v xz' + +log_info "=== RESULTS: ${PASS} passed, ${FAIL} failed ===" +[[ "${FAIL}" -eq 0 ]] \ + && log_info "All Tier 2 tests passed." \ + || log_warn "${FAIL} test(s) failed — review ${LOG_FILE}" +exit "${FAIL}" diff --git a/tests/test_minimal.sh b/tests/test_minimal.sh new file mode 100644 index 0000000..ffe17ea --- /dev/null +++ b/tests/test_minimal.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Created by John A. Hoeven with the ethical assistance of Claude AI +# --------------------------------------------------------------------------- +# test_minimal.sh +# ~/projects/ceos/alpine-installer/tests/test_minimal.sh +# Version: v0.0.1 | Status: DEVELOPMENT +# Target: Alpine Linux x86-64 (tinkerpad / ThinkPad T480) +# --------------------------------------------------------------------------- +# Re-runnable test suite for Tier 1 (Minimal). Run after ce-minimal.sh. +# Exits with count of failures (0 = all passed). +# --------------------------------------------------------------------------- + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT_NAME="test_minimal" +source "${SCRIPT_DIR}/lib/ce-common.sh" || { echo "ERROR: ce-common.sh not found"; exit 1; } + +PASS=0 +FAIL=0 + +check() { + local desc="${1}" cmd="${2}" + if eval "${cmd}" >/dev/null 2>&1; then + log_info "PASS: ${desc}" + ((PASS++)) + else + log_warn "FAIL: ${desc}" + ((FAIL++)) + fi +} + +log_info "=== TEST SUITE: TIER 1 MINIMAL ===" + +# Shell environment +check "starship present" 'command -v starship' +check "less present" 'command -v less' +check "mandoc installed" 'apk info -e mandoc' + +# Terminal +check "tmux present" 'command -v tmux' +check "vim present" 'command -v vim' +check "nano present" 'command -v nano' + +# Files +check "ranger present" 'command -v ranger' +check "fzf present" 'command -v fzf' +check "zoxide present" 'command -v zoxide' +check "stow present" 'command -v stow' + +# VCS and transfer +check "git present" 'command -v git' +check "rsync present" 'command -v rsync' +check "gpg present" 'command -v gpg' + +# System tools +check "btop present" 'command -v btop' +check "lsof present" 'command -v lsof' +check "dialog present" 'command -v dialog' + +# Comms +check "neomutt present" 'command -v neomutt' +check "msmtp present" 'command -v msmtp' +check "mbsync present" 'command -v mbsync' +check "pass present" 'command -v pass' + +# Journal +check "python3 present" 'command -v python3' +check "pipx present" 'command -v pipx' + +# Web +check "lynx present" 'command -v lynx' +check "w3m present" 'command -v w3m' + +# Network services +check "avahi-daemon present" 'command -v avahi-daemon' +check "dbus-daemon present" 'command -v dbus-daemon' + +# Recording +check "asciinema present" 'command -v asciinema' + +# Accessibility +check "espeak-ng present" 'command -v espeak-ng' + +log_info "=== RESULTS: ${PASS} passed, ${FAIL} failed ===" +[[ "${FAIL}" -eq 0 ]] \ + && log_info "All Tier 1 tests passed." \ + || log_warn "${FAIL} test(s) failed — review ${LOG_FILE}" +exit "${FAIL}"