diff --git a/src/lib/scripts/arch.ts b/src/lib/scripts/arch.ts index cae38b1..fe69ff4 100644 --- a/src/lib/scripts/arch.ts +++ b/src/lib/scripts/arch.ts @@ -1,5 +1,3 @@ -// Arch script - handles both pacman and AUR packages - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { isAurPackage } from '../aur'; @@ -7,104 +5,77 @@ export function generateArchScript(packages: PackageInfo[], helper: 'yay' | 'par const aurPackages = packages.filter(p => isAurPackage(p.pkg)); const officialPackages = packages.filter(p => !isAurPackage(p.pkg)); - return generateAsciiHeader('Arch Linux', packages.length) + generateSharedUtils(packages.length) + ` + return generateAsciiHeader('Arch Linux', packages.length) + generateSharedUtils('arch', packages.length) + ` is_installed() { pacman -Qi "$1" &>/dev/null; } -install_pacman() { - local name=$1 pkg=$2 +install_pkg() { + local name=$1 pkg=$2 cmd=$3 CURRENT=$((CURRENT + 1)) - + if is_installed "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - local output - if output=$(with_retry sudo pacman -S --needed --noconfirm "$pkg"); then + with_retry $cmd -S --needed --noconfirm "$pkg" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - if echo "$output" | grep -q "target not found"; then - echo -e " \${DIM}Package not found\${NC}" - elif echo "$output" | grep -q "signature"; then - echo -e " \${DIM}GPG issue - try: sudo pacman-key --refresh-keys\${NC}" - fi + printf "\\r\\033[K" >&3 + error "$name" FAILED+=("$name") fi } -install_aur() { - local name=$1 pkg=$2 - CURRENT=$((CURRENT + 1)) - - if is_installed "$pkg"; then - skip "$name" - SKIPPED+=("$name") - return 0 - fi - - show_progress $CURRENT $TOTAL "$name" - local start=$(date +%s) - - local output - if output=$(with_retry ${helper} -S --needed --noconfirm "$pkg"); then - local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" - SUCCEEDED+=("$name") - else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - if echo "$output" | grep -q "target not found"; then - echo -e " \${DIM}Package not found in AUR\${NC}" - fi - FAILED+=("$name") - fi -} +# --------------------------------------------------------------------------- -# ───────────────────────────────────────────────────────────────────────────── +[ "$EUID" -eq 0 ] && { error "Do not run as root."; exit 1; } -[ "$EUID" -eq 0 ] && { error "Run as regular user, not root."; exit 1; } +info "Caching sudo credentials..." +sudo -v || exit 1 +while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & -while [ -f /var/lib/pacman/db.lck ]; do - warn "Waiting for pacman lock..." - sleep 2 -done +wait_for_lock /var/lib/pacman/db.lck -info "Syncing databases..." -with_retry sudo pacman -Sy --noconfirm >/dev/null && success "Synced" || warn "Sync failed, continuing..." +info "Syncing package databases..." +with_retry sudo pacman -Sy --noconfirm & +if animate_progress "Syncing..." $!; then + printf "\\r\\033[K" >&3 + success "Synced" +else + printf "\\r\\033[K" >&3 + warn "Sync failed, continuing..." +fi ${aurPackages.length > 0 ? ` if ! command -v ${helper} &>/dev/null; then - warn "Installing ${helper} for AUR packages..." + warn "${helper} not found, installing for AUR packages..." sudo pacman -S --needed --noconfirm git base-devel >/dev/null 2>&1 tmp=$(mktemp -d) - git clone https://aur.archlinux.org/${helper}.git "$tmp/${helper}" >/dev/null 2>&1 + trap 'rm -rf "$tmp"' EXIT + git clone "https://aur.archlinux.org/${helper}.git" "$tmp/${helper}" >/dev/null 2>&1 (cd "$tmp/${helper}" && makepkg -si --noconfirm >/dev/null 2>&1) - rm -rf "$tmp" - command -v ${helper} &>/dev/null && success "${helper} installed" || warn "${helper} install failed" + command -v ${helper} &>/dev/null && success "${helper} ready" || { error "Failed to install ${helper}"; exit 1; } fi ` : ''} - -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 -${officialPackages.map(({ app, pkg }) => `install_pacman "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} +${officialPackages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}" "sudo pacman"`).join('\n')} ${aurPackages.length > 0 ? ` if command -v ${helper} &>/dev/null; then -${aurPackages.map(({ app, pkg }) => ` install_aur "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} +${aurPackages.map(({ app, pkg }) => ` install_pkg "${escapeShellString(app.name)}" "${pkg}" "${helper}"`).join('\n')} fi ` : ''} - print_summary `; } diff --git a/src/lib/scripts/debian.ts b/src/lib/scripts/debian.ts index 46c3b56..5a8005b 100644 --- a/src/lib/scripts/debian.ts +++ b/src/lib/scripts/debian.ts @@ -1,72 +1,74 @@ -// Debian script - apt-get with dependency auto-fix - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateDebianScript(packages: PackageInfo[]): string { - return generateAsciiHeader('Debian', packages.length) + generateSharedUtils(packages.length) + ` -is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } + return generateAsciiHeader('Debian', packages.length) + generateSharedUtils('debian', packages.length) + ` +export DEBIAN_FRONTEND=noninteractive -fix_deps() { - if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then - success "Dependencies fixed" - return 0 - fi - return 1 -} +is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } install_pkg() { local name=$1 pkg=$2 CURRENT=$((CURRENT + 1)) - + if is_installed "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - local output - if output=$(with_retry sudo apt-get install -y "$pkg"); then + + with_retry sudo apt-get install -y "$pkg" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - if echo "$output" | grep -q "Unable to locate"; then - echo -e " \${DIM}Package not found\${NC}" - elif echo "$output" | grep -q "unmet dependencies"; then - echo -e " \${DIM}Fixing dependencies...\${NC}" - if fix_deps; then - if sudo apt-get install -y "$pkg" >/dev/null 2>&1; then - timing "$name" "$(($(date +%s) - start))" + printf "\\r\\033[K" >&3 + if tail -n 50 "$LOG" | grep -q "unmet dependencies"; then + warn "Fixing dependencies for $name..." + if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then + sudo apt-get install -y "$pkg" & + if animate_progress "$name (retry)" $!; then + local elapsed=$(($(date +%s) - start)) + success "$name" "\${elapsed}s, deps fixed" SUCCEEDED+=("$name") return 0 fi fi fi + error "$name" FAILED+=("$name") fi } -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -[ "$EUID" -eq 0 ] && { error "Run as regular user, not root."; exit 1; } +[ "$EUID" -eq 0 ] && { error "Do not run as root."; exit 1; } -while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do - warn "Waiting for package manager..." - sleep 2 -done +info "Caching sudo credentials..." +sudo -v || exit 1 +while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & + +wait_for_lock /var/lib/dpkg/lock-frontend +sudo dpkg --configure -a >/dev/null 2>&1 || true info "Updating package lists..." -with_retry sudo apt-get update -qq >/dev/null && success "Updated" || warn "Update failed, continuing..." +with_retry sudo apt-get update -qq & +if animate_progress "Updating..." $!; then + printf "\\r\\033[K" >&3 + success "Updated" +else + printf "\\r\\033[K" >&3 + warn "Update failed, continuing..." +fi -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} diff --git a/src/lib/scripts/fedora.ts b/src/lib/scripts/fedora.ts index 047e1bb..39a8eed 100644 --- a/src/lib/scripts/fedora.ts +++ b/src/lib/scripts/fedora.ts @@ -1,61 +1,48 @@ -// Fedora script - dnf with RPM Fusion auto-enable - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateFedoraScript(packages: PackageInfo[]): string { - const rpmFusionPkgs = ['steam', 'vlc', 'ffmpeg', 'obs-studio']; - const needsRpmFusion = packages.some(p => rpmFusionPkgs.includes(p.pkg)); - - return generateAsciiHeader('Fedora', packages.length) + generateSharedUtils(packages.length) + ` + return generateAsciiHeader('Fedora', packages.length) + generateSharedUtils('fedora', packages.length) + ` is_installed() { rpm -q "$1" &>/dev/null; } install_pkg() { local name=$1 pkg=$2 CURRENT=$((CURRENT + 1)) - + if is_installed "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - local output - if output=$(with_retry sudo dnf install -y "$pkg"); then + + with_retry sudo dnf install -y "$pkg" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - if echo "$output" | grep -q "No match"; then - echo -e " \${DIM}Package not found\${NC}" - fi + printf "\\r\\033[K" >&3 + error "$name" FAILED+=("$name") fi } -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -[ "$EUID" -eq 0 ] && { error "Run as regular user, not root."; exit 1; } +[ "$EUID" -eq 0 ] && { error "Do not run as root."; exit 1; } command -v dnf &>/dev/null || { error "dnf not found"; exit 1; } -${needsRpmFusion ? ` -if ! dnf repolist 2>/dev/null | grep -q rpmfusion; then - info "Enabling RPM Fusion..." - sudo dnf install -y \\ - "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm" \\ - "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm" \\ - >/dev/null 2>&1 && success "RPM Fusion enabled" -fi -` : ''} +info "Caching sudo credentials..." +sudo -v || exit 1 +while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} diff --git a/src/lib/scripts/flatpak.ts b/src/lib/scripts/flatpak.ts index 1aefa53..fa5ec0e 100644 --- a/src/lib/scripts/flatpak.ts +++ b/src/lib/scripts/flatpak.ts @@ -1,113 +1,63 @@ -// Flatpak script - parallel install when 3+ packages - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateFlatpakScript(packages: PackageInfo[]): string { - const parallel = packages.length >= 3; + return generateAsciiHeader('Flatpak', packages.length) + generateSharedUtils('flatpak', packages.length) + ` +is_installed() { flatpak list --app --columns=application 2>/dev/null | grep -Fxq "$1"; } - return generateAsciiHeader('Flatpak', packages.length) + generateSharedUtils(packages.length) + ` -is_installed() { flatpak list --app 2>/dev/null | grep -q "$1"; } - -${parallel ? ` -# Parallel install for Flatpak -install_parallel() { - local pids=() - local names=() - local start=$(date +%s) - - for pair in "$@"; do - local name="\${pair%%|*}" - local appid="\${pair##*|}" - - if is_installed "$appid"; then - skip "$name" - SKIPPED+=("$name") - continue - fi - - (flatpak install flathub -y "$appid" >/dev/null 2>&1) & - pids+=($!) - names+=("$name") - done - - local total=\${#pids[@]} - local done_count=0 - - if [ $total -eq 0 ]; then - return - fi - - info "Installing $total apps in parallel..." - - for i in "\${!pids[@]}"; do - wait \${pids[$i]} - local status=$? - done_count=$((done_count + 1)) - - if [ $status -eq 0 ]; then - SUCCEEDED+=("\${names[$i]}") - success "\${names[$i]}" - else - FAILED+=("\${names[$i]}") - error "\${names[$i]} failed" - fi - done - - local elapsed=$(($(date +%s) - start)) - echo -e "\${DIM}Parallel install took \${elapsed}s\${NC}" -} -` : ` install_pkg() { local name=$1 appid=$2 CURRENT=$((CURRENT + 1)) - + if is_installed "$appid"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - if with_retry flatpak install flathub -y "$appid" >/dev/null; then + + with_retry flatpak install flathub -y "$appid" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" + printf "\\r\\033[K" >&3 + error "$name" FAILED+=("$name") fi } -`} -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -command -v flatpak &>/dev/null || { +command -v flatpak &>/dev/null || { error "Flatpak not installed" info "Install: sudo apt/dnf/pacman install flatpak" exit 1 } +# We only ask for sudo if we need to add the repo, since user flatpak installs generally don't need root if ! flatpak remotes 2>/dev/null | grep -q flathub; then + info "Caching sudo credentials to add Flathub..." + sudo -v || exit 1 + while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & + info "Adding Flathub..." - flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo >/dev/null 2>&1 success "Flathub added" fi -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 -${parallel - ? `install_parallel ${packages.map(({ app, pkg }) => `"${escapeShellString(app.name)}|${pkg}"`).join(' ')}` - : packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n') - } +${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} print_summary -echo -info "Restart session for apps in menu." +echo >&3 +info "Restart session for new apps to appear in menu." >&3 `; } diff --git a/src/lib/scripts/homebrew.ts b/src/lib/scripts/homebrew.ts index df563de..3642826 100644 --- a/src/lib/scripts/homebrew.ts +++ b/src/lib/scripts/homebrew.ts @@ -1,118 +1,95 @@ -// Homebrew script - brew with cask support for macOS + formulae for both platforms - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateHomebrewScript(packages: PackageInfo[]): string { - return generateAsciiHeader('Homebrew', packages.length) + generateSharedUtils(packages.length) + ` -# Platform detection + return generateAsciiHeader('Homebrew', packages.length) + generateSharedUtils('homebrew', packages.length) + ` +export HOMEBREW_NO_AUTO_UPDATE=1 + IS_MACOS=false if [[ "$OSTYPE" == "darwin"* ]]; then IS_MACOS=true fi -# Safety check: Homebrew should not be run as root -if [ "$EUID" -eq 0 ]; then - error "Homebrew should not be run as root. Please run as a normal user." - exit 1 -fi +[ "$EUID" -eq 0 ] && { error "Do not run Homebrew as root."; exit 1; } is_installed() { - local type=$1 - local pkg=$2 - # brew list returns 0 if installed, 1 if not. Suppress output. - # We use grep -Fxq for exact line matching to handle regex chars in names - if [ "$type" == "--cask" ]; then + local type=$1 pkg=$2 + if [ "$type" = "--cask" ]; then brew list --cask 2>/dev/null | grep -Fxq "$pkg" else brew list --formula 2>/dev/null | grep -Fxq "$pkg" fi } -install_package() { - local name=$1 - local pkg=$2 - local type=$3 # "" (formula) or "--cask" - +install_pkg() { + local name=$1 pkg=$2 type=$3 + CURRENT=$((CURRENT + 1)) - - # Casks are macOS only - if [ "$type" == "--cask" ] && [ "$IS_MACOS" = false ]; then - # Silent skip or minimal output for cleaner Linux logs? - # Using minimal output to let user know it was skipped intentionally - printf "\\r\\033[K\${YELLOW}○\${NC} %s \${DIM}(cask skipped on Linux)\${NC}\\n" "$name" + + if [ "$type" = "--cask" ] && [ "$IS_MACOS" = false ]; then + skip "$name" "cask, macOS only" SKIPPED+=("$name") return 0 fi - + if is_installed "$type" "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - # Construct brew command + local cmd="brew install" - if [ "$type" == "--cask" ]; then - cmd="brew install --cask" - fi - - local output - if output=$(with_retry $cmd "$pkg" 2>&1); then + [ "$type" = "--cask" ] && cmd="brew install --cask" + + with_retry $cmd "$pkg" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - - # Friendly error messages - if echo "$output" | grep -q "No available formula"; then - echo -e " \${DIM}Formula not found\${NC}" - elif echo "$output" | grep -q "No Cask with this name"; then - echo -e " \${DIM}Cask not found\${NC}" - fi - + printf "\\r\\033[K" >&3 + error "$name" FAILED+=("$name") fi } -# ───────────────────────────────────────────────────────────────────────────── -# Pre-flight -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -command -v brew &>/dev/null || { +command -v brew &>/dev/null || { error "Homebrew not found. Install from https://brew.sh" - exit 1 + exit 1 } if [ "$IS_MACOS" = true ]; then - info "Detected macOS" + info "Detected macOS" >&3 else - info "Detected Linux - formulae only (casks will be skipped)" + info "Detected Linux (casks will be skipped)" >&3 fi info "Updating Homebrew..." -# Run update silently; on error warn but continue (network flakes shouldn't block install) -brew update >/dev/null 2>&1 && success "Updated" || warn "Update failed, continuing..." +with_retry brew update & +if animate_progress "Updating..." $!; then + printf "\\r\\033[K" >&3 + success "Updated" +else + printf "\\r\\033[K" >&3 + warn "Update failed, continuing..." +fi -# ───────────────────────────────────────────────────────────────────────────── -# Installation -# ───────────────────────────────────────────────────────────────────────────── - -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 ${packages.map(({ app, pkg }) => { if (pkg.startsWith('--cask ')) { const caskName = pkg.replace('--cask ', ''); - return `install_package "${escapeShellString(app.name)}" "${caskName}" "--cask"`; + return `install_pkg "${escapeShellString(app.name)}" "${caskName}" "--cask"`; } - return `install_package "${escapeShellString(app.name)}" "${pkg}" ""`; + return `install_pkg "${escapeShellString(app.name)}" "${pkg}" ""`; }).join('\n')} print_summary diff --git a/src/lib/scripts/nix.ts b/src/lib/scripts/nix.ts index 2c39286..7f0d961 100644 --- a/src/lib/scripts/nix.ts +++ b/src/lib/scripts/nix.ts @@ -1,5 +1,3 @@ -// Nix declarative config generator - import { type PackageInfo } from './shared'; import { isUnfreePackage } from '../nixUnfree'; @@ -7,16 +5,19 @@ export function generateNixConfig(packages: PackageInfo[]): string { const validPackages = packages.filter(p => p.pkg.trim()); if (validPackages.length === 0) return '# No packages selected'; + const date = new Date().toISOString().split('T')[0]; const sortedPkgs = validPackages.map(p => p.pkg.trim()).sort(); const packageList = sortedPkgs.map(pkg => ` ${pkg}`).join('\n'); - // Add unfree warning if needed const unfreePkgs = sortedPkgs.filter(pkg => isUnfreePackage(pkg)); const unfreeComment = unfreePkgs.length > 0 ? `# Unfree: ${unfreePkgs.join(', ')}\n# Requires: nixpkgs.config.allowUnfree = true;\n\n` : ''; - return `${unfreeComment}environment.systemPackages = with pkgs; [ + return `# Generated by tuxmate — ${date} +# https://github.com/abusoww/tuxmate + +${unfreeComment}environment.systemPackages = with pkgs; [ ${packageList} ];`; } diff --git a/src/lib/scripts/opensuse.ts b/src/lib/scripts/opensuse.ts index 337fccd..6a7aa60 100644 --- a/src/lib/scripts/opensuse.ts +++ b/src/lib/scripts/opensuse.ts @@ -1,53 +1,64 @@ -// openSUSE script - zypper - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateOpenSUSEScript(packages: PackageInfo[]): string { - return generateAsciiHeader('openSUSE', packages.length) + generateSharedUtils(packages.length) + ` + return generateAsciiHeader('openSUSE', packages.length) + generateSharedUtils('opensuse', packages.length) + ` is_installed() { rpm -q "$1" &>/dev/null; } install_pkg() { local name=$1 pkg=$2 CURRENT=$((CURRENT + 1)) - + if is_installed "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - local output - if output=$(with_retry sudo zypper --non-interactive install --auto-agree-with-licenses "$pkg"); then + + with_retry sudo zypper --non-interactive install --auto-agree-with-licenses "$pkg" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" + printf "\\r\\033[K" >&3 + error "$name" FAILED+=("$name") fi } -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -[ "$EUID" -eq 0 ] && { error "Run as regular user, not root."; exit 1; } +[ "$EUID" -eq 0 ] && { error "Do not run as root."; exit 1; } command -v zypper &>/dev/null || { error "zypper not found"; exit 1; } -while [ -f /var/run/zypp.pid ]; do - warn "Waiting for zypper..." - sleep 2 -done +info "Caching sudo credentials..." +sudo -v || exit 1 +while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & -info "Refreshing repos..." -with_retry sudo zypper --non-interactive refresh >/dev/null && success "Refreshed" || warn "Refresh failed" +if [ -f /run/zypp.pid ]; then + wait_for_lock /run/zypp.pid +elif [ -f /var/run/zypp.pid ]; then + wait_for_lock /var/run/zypp.pid +fi -echo +info "Refreshing repositories..." +with_retry sudo zypper --non-interactive refresh & +if animate_progress "Refreshing..." $!; then + printf "\\r\\033[K" >&3 + success "Refreshed" +else + printf "\\r\\033[K" >&3 + warn "Refresh failed, continuing..." +fi + +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} diff --git a/src/lib/scripts/shared.ts b/src/lib/scripts/shared.ts index 055c7c3..4e32cbd 100644 --- a/src/lib/scripts/shared.ts +++ b/src/lib/scripts/shared.ts @@ -1,5 +1,3 @@ -// Stuff shared across all distro script generators - import { apps, type DistroId, type AppData } from '../data'; export interface PackageInfo { @@ -7,14 +5,13 @@ export interface PackageInfo { pkg: string; } -// Don't let anyone sneak shell commands through app names :) export function escapeShellString(str: string): string { return str - .replace(/\\/g, '\\\\') // Escape backslashes first - .replace(/"/g, '\\"') // Escape double quotes - .replace(/\$/g, '\\$') // Escape dollar signs - .replace(/`/g, '\\`') // Escape backticks - .replace(/!/g, '\\!'); // Escape history expansion + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\$/g, '\\$') + .replace(/`/g, '\\`') + .replace(/!/g, '\\!'); } export function getSelectedPackages(selectedAppIds: Set, distroId: DistroId): PackageInfo[] { @@ -42,143 +39,146 @@ export function generateAsciiHeader(distroName: string, pkgCount: number): strin # Packages: ${pkgCount} # Generated: ${date} # -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- set -euo pipefail +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +export LC_ALL=C +umask 077 + `; } -export function generateSharedUtils(total: number): string { - return `# ───────────────────────────────────────────────────────────────────────────── -# Colors & Utilities -# ───────────────────────────────────────────────────────────────────────────── +export function generateSharedUtils(distroName: string, total: number): string { + return `# --------------------------------------------------------------------------- +# Logging & Colors +# --------------------------------------------------------------------------- -if [ -t 1 ]; then +LOG="/tmp/tuxmate-${distroName.toLowerCase().replace(/\s+/g, '-')}-$(date +%Y%m%d-%H%M%S).log" +# Save original stdout to FD 3 +exec 3>&1 +# Redirect script's stdout & stderr to the log file to keep TTY clean +exec > "$LOG" 2>&1 + +if [ -t 3 ]; then RED='\\033[0;31m' GREEN='\\033[0;32m' YELLOW='\\033[1;33m' BLUE='\\033[0;34m' CYAN='\\033[0;36m' BOLD='\\033[1m' DIM='\\033[2m' NC='\\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC='' fi -info() { echo -e "\${BLUE}::\${NC} $1"; } -success() { echo -e "\${GREEN}✓\${NC} $1"; } -warn() { echo -e "\${YELLOW}!\${NC} $1"; } -error() { echo -e "\${RED}✗\${NC} $1" >&2; } -skip() { echo -e "\${DIM}○\${NC} $1 \${DIM}(already installed)\${NC}"; } -timing() { echo -e "\${GREEN}✓\${NC} $1 \${DIM}($2s)\${NC}"; } +# Print visually to FD 3 (the user's terminal) +info() { echo -e "\${BLUE}::\${NC} $1" >&3; echo ":: $1"; } +success() { + if [ -n "\${2:-}" ]; then + echo -e "\${GREEN}[+]\${NC} $1 \${DIM}(\$2)\${NC}" >&3 + echo "[+] $1 (\$2)" + else + echo -e "\${GREEN}[+]\${NC} $1" >&3 + echo "[+] $1" + fi +} +warn() { echo -e "\${YELLOW}[!]\${NC} $1" >&3; echo "[!] $1"; } +error() { echo -e "\${RED}[x]\${NC} $1" >&3; echo "[x] $1" >&2; } +skip() { + local reason="\${2:-already installed}" + echo -e "\${DIM}[-]\${NC} $1 \${DIM}(\$reason)\${NC}" >&3 + echo "[-] $1 (\$reason)" +} -# Graceful exit on Ctrl+C -trap 'printf "\\n"; warn "Installation cancelled by user"; print_summary; exit 130' INT +trap 'printf "\\n" >&3; warn "Cancelled by user"; print_summary; exit 130' INT TOTAL=${total} CURRENT=0 FAILED=() SUCCEEDED=() SKIPPED=() -INSTALL_TIMES=() START_TIME=$(date +%s) -AVG_TIME=8 # Initial estimate: 8 seconds per package -show_progress() { - local current=$1 total=$2 name=$3 - local percent=$((current * 100 / total)) - local filled=$((percent / 5)) - local empty=$((20 - filled)) +animate_progress() { + local name=$1 pid=$2 + local start=$(date +%s) + local spinstr='|/-\\' + local spin_idx=0 - # Calculate ETA - local remaining=$((total - current)) - local eta=$((remaining * AVG_TIME)) - local eta_str="" - if [ $eta -ge 60 ]; then - eta_str="~$((eta / 60))m" - else - eta_str="~\${eta}s" - fi - - printf "\\r\\033[K[\${CYAN}" - printf "%\${filled}s" | tr ' ' '█' - printf "\${NC}" - printf "%\${empty}s" | tr ' ' '░' - printf "] %3d%% (%d/%d) \${BOLD}%s\${NC} \${DIM}%s left\${NC}" "$percent" "$current" "$total" "$name" "$eta_str" + while kill -0 $pid 2>/dev/null; do + local elapsed=$(($(date +%s) - start)) + local percent=$((CURRENT * 100 / TOTAL)) + local filled=$((percent / 5)) + local empty=$((20 - filled)) + + local hash="####################" + local dash="--------------------" + local bar="\${CYAN}\${hash:0:filled}\${NC}\${dash:0:empty}" + local spin_char="\${spinstr:$spin_idx:1}" + spin_idx=$(( (spin_idx + 1) % 4 )) + + printf "\\r\\033[K[%b] %3d%% (%d/%d) \${BOLD}%s\${NC} [%c] %ds" "$bar" "$percent" "$CURRENT" "$TOTAL" "$name" "$spin_char" "$elapsed" >&3 + + sleep 0.1 + done + wait $pid + return $? } -# Update average install time -update_avg_time() { - local new_time=$1 - if [ \${#INSTALL_TIMES[@]} -eq 0 ]; then - AVG_TIME=$new_time - else - local sum=$new_time - for t in "\${INSTALL_TIMES[@]}"; do - sum=$((sum + t)) - done - AVG_TIME=$((sum / (\${#INSTALL_TIMES[@]} + 1))) - fi - INSTALL_TIMES+=($new_time) -} - -# Safe command executor (no eval) -run_cmd() { - "$@" 2>&1 -} - -# Network retry wrapper - uses run_cmd for safety with_retry() { - local max_attempts=3 - local attempt=1 - local delay=5 - - while [ $attempt -le $max_attempts ]; do - if output=$(run_cmd "$@"); then - echo "$output" - return 0 + local attempt=1 max=3 delay=5 + while [ $attempt -le $max ]; do + echo "=== Executing (Attempt $attempt/$max): $* ===" + if "$@"; then return 0; fi + echo "=== Command failed ===" + if [ $attempt -lt $max ]; then + echo "Retrying in \${delay}s..." + sleep $delay + delay=$((delay * 2)) fi - - # Check for network errors - if echo "$output" | grep -qiE "network|connection|timeout|unreachable|resolve"; then - if [ $attempt -lt $max_attempts ]; then - warn "Network error, retrying in \${delay}s... (attempt $attempt/$max_attempts)" - sleep $delay - delay=$((delay * 2)) - attempt=$((attempt + 1)) - continue - fi - fi - - echo "$output" - return 1 + attempt=$((attempt + 1)) done return 1 } +wait_for_lock() { + local file=$1 timeout=60 elapsed=0 + while [ -f "$file" ] || fuser "$file" >/dev/null 2>&1; do + if [ $elapsed -ge $timeout ]; then + error "Lock timeout after \${timeout}s: $file" + exit 1 + fi + warn "Waiting for lock: $file" + sleep 2 + elapsed=$((elapsed + 2)) + done +} + print_summary() { local end_time=$(date +%s) local duration=$((end_time - START_TIME)) local mins=$((duration / 60)) local secs=$((duration % 60)) - - echo - echo "─────────────────────────────────────────────────────────────────────────────" + + echo >&3 + echo "---------------------------------------------------------------------------" >&3 local installed=\${#SUCCEEDED[@]} local skipped_count=\${#SKIPPED[@]} local failed_count=\${#FAILED[@]} - + if [ $failed_count -eq 0 ]; then if [ $skipped_count -gt 0 ]; then - echo -e "\${GREEN}✓\${NC} Done! $installed installed, $skipped_count already installed \${DIM}(\${mins}m \${secs}s)\${NC}" + echo -e "\${GREEN}[+]\${NC} Done: $installed installed, $skipped_count already present \${DIM}(\${mins}m \${secs}s)\${NC}" >&3 else - echo -e "\${GREEN}✓\${NC} All $TOTAL packages installed! \${DIM}(\${mins}m \${secs}s)\${NC}" + echo -e "\${GREEN}[+]\${NC} All $TOTAL packages installed \${DIM}(\${mins}m \${secs}s)\${NC}" >&3 fi else - echo -e "\${YELLOW}!\${NC} $installed installed, $skipped_count skipped, $failed_count failed \${DIM}(\${mins}m \${secs}s)\${NC}" - echo - echo -e "\${RED}Failed:\${NC}" + echo -e "\${YELLOW}[!]\${NC} $installed installed, $skipped_count skipped, $failed_count failed \${DIM}(\${mins}m \${secs}s)\${NC}" >&3 + echo >&3 + echo -e "\${RED}Failed:\${NC}" >&3 for pkg in "\${FAILED[@]}"; do - echo " • $pkg" + echo " - $pkg" >&3 done fi - echo "─────────────────────────────────────────────────────────────────────────────" + echo "---------------------------------------------------------------------------" >&3 + echo -e "\${DIM}Log: $LOG\${NC}" >&3 } `; diff --git a/src/lib/scripts/snap.ts b/src/lib/scripts/snap.ts index ef406ed..6217f7d 100644 --- a/src/lib/scripts/snap.ts +++ b/src/lib/scripts/snap.ts @@ -1,62 +1,65 @@ -// Snap script - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateSnapScript(packages: PackageInfo[]): string { - return generateAsciiHeader('Snap', packages.length) + generateSharedUtils(packages.length) + ` + return generateAsciiHeader('Snap', packages.length) + generateSharedUtils('snap', packages.length) + ` is_installed() { - local snap_name=$(echo "$1" | awk '{print $1}') + local snap_name + snap_name=$(echo "$1" | awk '{print $1}') snap list 2>/dev/null | grep -q "^$snap_name " } install_pkg() { local name=$1 pkg=$2 CURRENT=$((CURRENT + 1)) - + if is_installed "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - local output - if output=$(with_retry sudo snap install $pkg); then + + # pkg is intentionally unquoted: it may contain flags like --classic + with_retry sudo snap install $pkg & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - if echo "$output" | grep -q "not found"; then - echo -e " \${DIM}Snap not found\${NC}" - fi + printf "\\r\\033[K" >&3 + error "$name" FAILED+=("$name") fi } -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -command -v snap &>/dev/null || { +command -v snap &>/dev/null || { error "Snap not installed" - info "Install: sudo apt/dnf/pacman install snapd" + info "Install: sudo apt/dnf/pacman install snapd" >&3 exit 1 } +info "Caching sudo credentials..." +sudo -v || exit 1 +while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & + if command -v systemctl &>/dev/null && ! systemctl is-active --quiet snapd; then info "Starting snapd..." - sudo systemctl enable --now snapd.socket - sudo systemctl start snapd + sudo systemctl enable --now snapd.socket >/dev/null 2>&1 + sudo systemctl start snapd >/dev/null 2>&1 sleep 2 + printf "\\r\\033[K" >&3 success "snapd started" fi -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} diff --git a/src/lib/scripts/ubuntu.ts b/src/lib/scripts/ubuntu.ts index 7abf9f4..bda9610 100644 --- a/src/lib/scripts/ubuntu.ts +++ b/src/lib/scripts/ubuntu.ts @@ -1,81 +1,74 @@ -// Ubuntu script - apt-get with dependency auto-fix - import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; export function generateUbuntuScript(packages: PackageInfo[]): string { - return generateAsciiHeader('Ubuntu', packages.length) + generateSharedUtils(packages.length) + ` -is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } + return generateAsciiHeader('Ubuntu', packages.length) + generateSharedUtils('ubuntu', packages.length) + ` +export DEBIAN_FRONTEND=noninteractive -# Auto-fix broken dependencies -fix_deps() { - if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then - success "Dependencies fixed" - return 0 - fi - return 1 -} +is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } install_pkg() { local name=$1 pkg=$2 CURRENT=$((CURRENT + 1)) - + if is_installed "$pkg"; then skip "$name" SKIPPED+=("$name") return 0 fi - - show_progress $CURRENT $TOTAL "$name" + local start=$(date +%s) - - local output - if output=$(with_retry sudo apt-get install -y "$pkg"); then + + with_retry sudo apt-get install -y "$pkg" & + local pid=$! + + if animate_progress "$name" $pid; then local elapsed=$(($(date +%s) - start)) - update_avg_time $elapsed - printf "\\r\\033[K" - timing "$name" "$elapsed" + printf "\\r\\033[K" >&3 + success "$name" "\${elapsed}s" SUCCEEDED+=("$name") else - printf "\\r\\033[K\${RED}✗\${NC} %s\\n" "$name" - if echo "$output" | grep -q "Unable to locate"; then - echo -e " \${DIM}Package not found\${NC}" - elif echo "$output" | grep -q "unmet dependencies"; then - echo -e " \${DIM}Fixing dependencies...\${NC}" - if fix_deps; then - # Retry once after fixing deps - if sudo apt-get install -y "$pkg" >/dev/null 2>&1; then - timing "$name" "$(($(date +%s) - start))" + printf "\\r\\033[K" >&3 + if tail -n 50 "$LOG" | grep -q "unmet dependencies"; then + warn "Fixing dependencies for $name..." >&3 + if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then + sudo apt-get install -y "$pkg" & + if animate_progress "$name (retry)" $!; then + local elapsed=$(($(date +%s) - start)) + success "$name" "\${elapsed}s, deps fixed" SUCCEEDED+=("$name") return 0 fi fi fi + error "$name" FAILED+=("$name") fi } -# ───────────────────────────────────────────────────────────────────────────── -# Pre-flight -# ───────────────────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- -[ "$EUID" -eq 0 ] && { error "Run as regular user, not root."; exit 1; } +[ "$EUID" -eq 0 ] && { error "Do not run as root."; exit 1; } -# Wait for apt lock -while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do - warn "Waiting for package manager..." - sleep 2 -done +info "Caching sudo credentials..." +sudo -v || exit 1 +while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null & + +wait_for_lock /var/lib/dpkg/lock-frontend +sudo dpkg --configure -a >/dev/null 2>&1 || true info "Updating package lists..." -with_retry sudo apt-get update -qq >/dev/null && success "Updated" || warn "Update failed, continuing..." +with_retry sudo apt-get update -qq & +if animate_progress "Updating..." $!; then + printf "\\r\\033[K" >&3 + success "Updated" +else + printf "\\r\\033[K" >&3 + warn "Update failed, continuing..." +fi -# ───────────────────────────────────────────────────────────────────────────── -# Installation -# ───────────────────────────────────────────────────────────────────────────── - -echo +echo >&3 info "Installing $TOTAL packages" -echo +echo >&3 ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} diff --git a/src/lib/verified-flatpaks.json b/src/lib/verified-flatpaks.json index 22dce5c..7b4cc58 100644 --- a/src/lib/verified-flatpaks.json +++ b/src/lib/verified-flatpaks.json @@ -1,8 +1,8 @@ { "meta": { - "fetchedAt": "2026-02-22T09:54:12.215Z" + "fetchedAt": "2026-02-23T09:27:17.164Z" }, - "count": 697, + "count": 695, "apps": [ "ai.jan.Jan", "app.bluebubbles.BlueBubbles", @@ -225,7 +225,6 @@ "fr.handbrake.ghb", "garden.jamie.Morphosis", "gg.minion.Minion", - "gg.norisk.NoRiskClientLauncherV3", "hu.irl.cameractrls", "im.bernard.Nostalgia", "im.dino.Dino", @@ -342,7 +341,6 @@ "io.github.nozwock.Packet", "io.github.nroduit.Weasis", "io.github.nuttyartist.notes", - "io.github.olaproeis.Ferrite", "io.github.onionware_github.onionmedia", "io.github.pantheon_tweaks.pantheon-tweaks", "io.github.peazip.PeaZip", @@ -399,7 +397,6 @@ "io.gitlab.theevilskeleton.Upscaler", "io.kapsa.drive", "io.kinvolk.Headlamp", - "io.m51.Gelly", "io.missioncenter.MissionCenter", "io.openrct2.OpenRCT2", "io.podman_desktop.PodmanDesktop", @@ -456,6 +453,7 @@ "org.altlinux.Tuner", "org.azahar_emu.Azahar", "org.bunkus.mkvtoolnix-gui", + "org.chessmd.chessmd", "org.cloudcompare.CloudCompare", "org.cockpit_project.CockpitClient", "org.contourterminal.Contour", @@ -630,7 +628,6 @@ "org.openrgb.OpenRGB", "org.openscad.OpenSCAD", "org.opensurge2d.OpenSurge", - "org.pencil2d.Pencil2D", "org.photoqt.PhotoQt", "org.pitivi.Pitivi", "org.ppsspp.PPSSPP", @@ -673,6 +670,7 @@ "page.codeberg.libre_menu_editor.LibreMenuEditor", "page.codeberg.lo_vely.Nucleus", "page.codeberg.tahoso.azul-box", + "page.codeberg.vendillah.GamepadMirror", "page.kramo.Cartridges", "page.tesk.Refine", "re.fossplant.songrec",