feat: implemented logging in scripts and fixed some logical problems

This commit is contained in:
N1C4T
2026-02-23 13:35:33 +04:00
parent 06ac033f94
commit a5cef79bbb
11 changed files with 369 additions and 476 deletions

View File

@@ -1,5 +1,3 @@
// Arch script - handles both pacman and AUR packages
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
import { isAurPackage } from '../aur'; 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 aurPackages = packages.filter(p => isAurPackage(p.pkg));
const officialPackages = 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; } is_installed() { pacman -Qi "$1" &>/dev/null; }
install_pacman() { install_pkg() {
local name=$1 pkg=$2 local name=$1 pkg=$2 cmd=$3
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$pkg"; then if is_installed "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
local output with_retry $cmd -S --needed --noconfirm "$pkg" &
if output=$(with_retry sudo pacman -S --needed --noconfirm "$pkg"); then local pid=$!
if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
if echo "$output" | grep -q "target not found"; then error "$name"
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
FAILED+=("$name") FAILED+=("$name")
fi 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 wait_for_lock /var/lib/pacman/db.lck
warn "Waiting for pacman lock..."
sleep 2
done
info "Syncing databases..." info "Syncing package databases..."
with_retry sudo pacman -Sy --noconfirm >/dev/null && success "Synced" || warn "Sync failed, continuing..." 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 ? ` ${aurPackages.length > 0 ? `
if ! command -v ${helper} &>/dev/null; then 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 sudo pacman -S --needed --noconfirm git base-devel >/dev/null 2>&1
tmp=$(mktemp -d) 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) (cd "$tmp/${helper}" && makepkg -si --noconfirm >/dev/null 2>&1)
rm -rf "$tmp" command -v ${helper} &>/dev/null && success "${helper} ready" || { error "Failed to install ${helper}"; exit 1; }
command -v ${helper} &>/dev/null && success "${helper} installed" || warn "${helper} install failed"
fi fi
` : ''} ` : ''}
echo >&3
echo
info "Installing $TOTAL packages" 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 ? ` ${aurPackages.length > 0 ? `
if command -v ${helper} &>/dev/null; then 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 fi
` : ''} ` : ''}
print_summary print_summary
`; `;
} }

View File

@@ -1,72 +1,74 @@
// Debian script - apt-get with dependency auto-fix
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateDebianScript(packages: PackageInfo[]): string { export function generateDebianScript(packages: PackageInfo[]): string {
return generateAsciiHeader('Debian', packages.length) + generateSharedUtils(packages.length) + ` return generateAsciiHeader('Debian', packages.length) + generateSharedUtils('debian', packages.length) + `
is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } export DEBIAN_FRONTEND=noninteractive
fix_deps() { is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; }
if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then
success "Dependencies fixed"
return 0
fi
return 1
}
install_pkg() { install_pkg() {
local name=$1 pkg=$2 local name=$1 pkg=$2
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$pkg"; then if is_installed "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
local output with_retry sudo apt-get install -y "$pkg" &
if output=$(with_retry sudo apt-get install -y "$pkg"); then local pid=$!
if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
if echo "$output" | grep -q "Unable to locate"; then if tail -n 50 "$LOG" | grep -q "unmet dependencies"; then
echo -e " \${DIM}Package not found\${NC}" warn "Fixing dependencies for $name..."
elif echo "$output" | grep -q "unmet dependencies"; then if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then
echo -e " \${DIM}Fixing dependencies...\${NC}" sudo apt-get install -y "$pkg" &
if fix_deps; then if animate_progress "$name (retry)" $!; then
if sudo apt-get install -y "$pkg" >/dev/null 2>&1; then local elapsed=$(($(date +%s) - start))
timing "$name" "$(($(date +%s) - start))" success "$name" "\${elapsed}s, deps fixed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
return 0 return 0
fi fi
fi fi
fi fi
error "$name"
FAILED+=("$name") FAILED+=("$name")
fi 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 info "Caching sudo credentials..."
warn "Waiting for package manager..." sudo -v || exit 1
sleep 2 while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
done
wait_for_lock /var/lib/dpkg/lock-frontend
sudo dpkg --configure -a >/dev/null 2>&1 || true
info "Updating package lists..." 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" info "Installing $TOTAL packages"
echo echo >&3
${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')}

View File

@@ -1,61 +1,48 @@
// Fedora script - dnf with RPM Fusion auto-enable
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateFedoraScript(packages: PackageInfo[]): string { export function generateFedoraScript(packages: PackageInfo[]): string {
const rpmFusionPkgs = ['steam', 'vlc', 'ffmpeg', 'obs-studio']; return generateAsciiHeader('Fedora', packages.length) + generateSharedUtils('fedora', packages.length) + `
const needsRpmFusion = packages.some(p => rpmFusionPkgs.includes(p.pkg));
return generateAsciiHeader('Fedora', packages.length) + generateSharedUtils(packages.length) + `
is_installed() { rpm -q "$1" &>/dev/null; } is_installed() { rpm -q "$1" &>/dev/null; }
install_pkg() { install_pkg() {
local name=$1 pkg=$2 local name=$1 pkg=$2
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$pkg"; then if is_installed "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
local output with_retry sudo dnf install -y "$pkg" &
if output=$(with_retry sudo dnf install -y "$pkg"); then local pid=$!
if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
if echo "$output" | grep -q "No match"; then error "$name"
echo -e " \${DIM}Package not found\${NC}"
fi
FAILED+=("$name") FAILED+=("$name")
fi 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; } command -v dnf &>/dev/null || { error "dnf not found"; exit 1; }
${needsRpmFusion ? ` info "Caching sudo credentials..."
if ! dnf repolist 2>/dev/null | grep -q rpmfusion; then sudo -v || exit 1
info "Enabling RPM Fusion..." while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
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
` : ''}
echo echo >&3
info "Installing $TOTAL packages" info "Installing $TOTAL packages"
echo echo >&3
${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')}

View File

@@ -1,113 +1,63 @@
// Flatpak script - parallel install when 3+ packages
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateFlatpakScript(packages: PackageInfo[]): string { 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() { install_pkg() {
local name=$1 appid=$2 local name=$1 appid=$2
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$appid"; then if is_installed "$appid"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) 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)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
error "$name"
FAILED+=("$name") FAILED+=("$name")
fi fi
} }
`}
# ───────────────────────────────────────────────────────────────────────────── # ---------------------------------------------------------------------------
command -v flatpak &>/dev/null || { command -v flatpak &>/dev/null || {
error "Flatpak not installed" error "Flatpak not installed"
info "Install: sudo apt/dnf/pacman install flatpak" info "Install: sudo apt/dnf/pacman install flatpak"
exit 1 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 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..." 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" success "Flathub added"
fi fi
echo echo >&3
info "Installing $TOTAL packages" info "Installing $TOTAL packages"
echo echo >&3
${parallel ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')}
? `install_parallel ${packages.map(({ app, pkg }) => `"${escapeShellString(app.name)}|${pkg}"`).join(' ')}`
: packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')
}
print_summary print_summary
echo echo >&3
info "Restart session for apps in menu." info "Restart session for new apps to appear in menu." >&3
`; `;
} }

View File

@@ -1,118 +1,95 @@
// Homebrew script - brew with cask support for macOS + formulae for both platforms
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateHomebrewScript(packages: PackageInfo[]): string { export function generateHomebrewScript(packages: PackageInfo[]): string {
return generateAsciiHeader('Homebrew', packages.length) + generateSharedUtils(packages.length) + ` return generateAsciiHeader('Homebrew', packages.length) + generateSharedUtils('homebrew', packages.length) + `
# Platform detection export HOMEBREW_NO_AUTO_UPDATE=1
IS_MACOS=false IS_MACOS=false
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
IS_MACOS=true IS_MACOS=true
fi fi
# Safety check: Homebrew should not be run as root [ "$EUID" -eq 0 ] && { error "Do not run Homebrew as root."; exit 1; }
if [ "$EUID" -eq 0 ]; then
error "Homebrew should not be run as root. Please run as a normal user."
exit 1
fi
is_installed() { is_installed() {
local type=$1 local type=$1 pkg=$2
local pkg=$2 if [ "$type" = "--cask" ]; then
# 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
brew list --cask 2>/dev/null | grep -Fxq "$pkg" brew list --cask 2>/dev/null | grep -Fxq "$pkg"
else else
brew list --formula 2>/dev/null | grep -Fxq "$pkg" brew list --formula 2>/dev/null | grep -Fxq "$pkg"
fi fi
} }
install_package() { install_pkg() {
local name=$1 local name=$1 pkg=$2 type=$3
local pkg=$2
local type=$3 # "" (formula) or "--cask"
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
# Casks are macOS only if [ "$type" = "--cask" ] && [ "$IS_MACOS" = false ]; then
if [ "$type" == "--cask" ] && [ "$IS_MACOS" = false ]; then skip "$name" "cask, macOS only"
# 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"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
if is_installed "$type" "$pkg"; then if is_installed "$type" "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
# Construct brew command
local cmd="brew install" local cmd="brew install"
if [ "$type" == "--cask" ]; then [ "$type" = "--cask" ] && cmd="brew install --cask"
cmd="brew install --cask"
fi with_retry $cmd "$pkg" &
local pid=$!
local output
if output=$(with_retry $cmd "$pkg" 2>&1); then if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
error "$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
FAILED+=("$name") FAILED+=("$name")
fi fi
} }
# ───────────────────────────────────────────────────────────────────────────── # ---------------------------------------------------------------------------
# Pre-flight
# ─────────────────────────────────────────────────────────────────────────────
command -v brew &>/dev/null || { command -v brew &>/dev/null || {
error "Homebrew not found. Install from https://brew.sh" error "Homebrew not found. Install from https://brew.sh"
exit 1 exit 1
} }
if [ "$IS_MACOS" = true ]; then if [ "$IS_MACOS" = true ]; then
info "Detected macOS" info "Detected macOS" >&3
else else
info "Detected Linux - formulae only (casks will be skipped)" info "Detected Linux (casks will be skipped)" >&3
fi fi
info "Updating Homebrew..." info "Updating Homebrew..."
# Run update silently; on error warn but continue (network flakes shouldn't block install) with_retry brew update &
brew update >/dev/null 2>&1 && success "Updated" || warn "Update failed, continuing..." if animate_progress "Updating..." $!; then
printf "\\r\\033[K" >&3
success "Updated"
else
printf "\\r\\033[K" >&3
warn "Update failed, continuing..."
fi
# ───────────────────────────────────────────────────────────────────────────── echo >&3
# Installation
# ─────────────────────────────────────────────────────────────────────────────
echo
info "Installing $TOTAL packages" info "Installing $TOTAL packages"
echo echo >&3
${packages.map(({ app, pkg }) => { ${packages.map(({ app, pkg }) => {
if (pkg.startsWith('--cask ')) { if (pkg.startsWith('--cask ')) {
const caskName = pkg.replace('--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')} }).join('\n')}
print_summary print_summary

View File

@@ -1,5 +1,3 @@
// Nix declarative config generator
import { type PackageInfo } from './shared'; import { type PackageInfo } from './shared';
import { isUnfreePackage } from '../nixUnfree'; import { isUnfreePackage } from '../nixUnfree';
@@ -7,16 +5,19 @@ export function generateNixConfig(packages: PackageInfo[]): string {
const validPackages = packages.filter(p => p.pkg.trim()); const validPackages = packages.filter(p => p.pkg.trim());
if (validPackages.length === 0) return '# No packages selected'; 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 sortedPkgs = validPackages.map(p => p.pkg.trim()).sort();
const packageList = sortedPkgs.map(pkg => ` ${pkg}`).join('\n'); const packageList = sortedPkgs.map(pkg => ` ${pkg}`).join('\n');
// Add unfree warning if needed
const unfreePkgs = sortedPkgs.filter(pkg => isUnfreePackage(pkg)); const unfreePkgs = sortedPkgs.filter(pkg => isUnfreePackage(pkg));
const unfreeComment = unfreePkgs.length > 0 const unfreeComment = unfreePkgs.length > 0
? `# Unfree: ${unfreePkgs.join(', ')}\n# Requires: nixpkgs.config.allowUnfree = true;\n\n` ? `# 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} ${packageList}
];`; ];`;
} }

View File

@@ -1,53 +1,64 @@
// openSUSE script - zypper
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateOpenSUSEScript(packages: PackageInfo[]): string { 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; } is_installed() { rpm -q "$1" &>/dev/null; }
install_pkg() { install_pkg() {
local name=$1 pkg=$2 local name=$1 pkg=$2
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$pkg"; then if is_installed "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
local output with_retry sudo zypper --non-interactive install --auto-agree-with-licenses "$pkg" &
if output=$(with_retry sudo zypper --non-interactive install --auto-agree-with-licenses "$pkg"); then local pid=$!
if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
error "$name"
FAILED+=("$name") FAILED+=("$name")
fi 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; } command -v zypper &>/dev/null || { error "zypper not found"; exit 1; }
while [ -f /var/run/zypp.pid ]; do info "Caching sudo credentials..."
warn "Waiting for zypper..." sudo -v || exit 1
sleep 2 while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
done
info "Refreshing repos..." if [ -f /run/zypp.pid ]; then
with_retry sudo zypper --non-interactive refresh >/dev/null && success "Refreshed" || warn "Refresh failed" 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" info "Installing $TOTAL packages"
echo echo >&3
${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')}

View File

@@ -1,5 +1,3 @@
// Stuff shared across all distro script generators
import { apps, type DistroId, type AppData } from '../data'; import { apps, type DistroId, type AppData } from '../data';
export interface PackageInfo { export interface PackageInfo {
@@ -7,14 +5,13 @@ export interface PackageInfo {
pkg: string; pkg: string;
} }
// Don't let anyone sneak shell commands through app names :)
export function escapeShellString(str: string): string { export function escapeShellString(str: string): string {
return str return str
.replace(/\\/g, '\\\\') // Escape backslashes first .replace(/\\/g, '\\\\')
.replace(/"/g, '\\"') // Escape double quotes .replace(/"/g, '\\"')
.replace(/\$/g, '\\$') // Escape dollar signs .replace(/\$/g, '\\$')
.replace(/`/g, '\\`') // Escape backticks .replace(/`/g, '\\`')
.replace(/!/g, '\\!'); // Escape history expansion .replace(/!/g, '\\!');
} }
export function getSelectedPackages(selectedAppIds: Set<string>, distroId: DistroId): PackageInfo[] { export function getSelectedPackages(selectedAppIds: Set<string>, distroId: DistroId): PackageInfo[] {
@@ -42,143 +39,146 @@ export function generateAsciiHeader(distroName: string, pkgCount: number): strin
# Packages: ${pkgCount} # Packages: ${pkgCount}
# Generated: ${date} # Generated: ${date}
# #
# ───────────────────────────────────────────────────────────────────────────── # ---------------------------------------------------------------------------
set -euo pipefail 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 { export function generateSharedUtils(distroName: string, total: number): string {
return `# ───────────────────────────────────────────────────────────────────────────── return `# ---------------------------------------------------------------------------
# Colors & Utilities # 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' 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' BLUE='\\033[0;34m' CYAN='\\033[0;36m' BOLD='\\033[1m' DIM='\\033[2m' NC='\\033[0m'
else else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC='' RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC=''
fi fi
info() { echo -e "\${BLUE}::\${NC} $1"; } # Print visually to FD 3 (the user's terminal)
success() { echo -e "\${GREEN}\${NC} $1"; } info() { echo -e "\${BLUE}::\${NC} $1" >&3; echo ":: $1"; }
warn() { echo -e "\${YELLOW}!\${NC} $1"; } success() {
error() { echo -e "\${RED}\${NC} $1" >&2; } if [ -n "\${2:-}" ]; then
skip() { echo -e "\${DIM}\${NC} $1 \${DIM}(already installed)\${NC}"; } echo -e "\${GREEN}[+]\${NC} $1 \${DIM}(\$2)\${NC}" >&3
timing() { echo -e "\${GREEN}\${NC} $1 \${DIM}($2s)\${NC}"; } 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" >&3; warn "Cancelled by user"; print_summary; exit 130' INT
trap 'printf "\\n"; warn "Installation cancelled by user"; print_summary; exit 130' INT
TOTAL=${total} TOTAL=${total}
CURRENT=0 CURRENT=0
FAILED=() FAILED=()
SUCCEEDED=() SUCCEEDED=()
SKIPPED=() SKIPPED=()
INSTALL_TIMES=()
START_TIME=$(date +%s) START_TIME=$(date +%s)
AVG_TIME=8 # Initial estimate: 8 seconds per package
show_progress() { animate_progress() {
local current=$1 total=$2 name=$3 local name=$1 pid=$2
local percent=$((current * 100 / total)) local start=$(date +%s)
local filled=$((percent / 5)) local spinstr='|/-\\'
local empty=$((20 - filled)) local spin_idx=0
# Calculate ETA while kill -0 $pid 2>/dev/null; do
local remaining=$((total - current)) local elapsed=$(($(date +%s) - start))
local eta=$((remaining * AVG_TIME)) local percent=$((CURRENT * 100 / TOTAL))
local eta_str="" local filled=$((percent / 5))
if [ $eta -ge 60 ]; then local empty=$((20 - filled))
eta_str="~$((eta / 60))m"
else local hash="####################"
eta_str="~\${eta}s" local dash="--------------------"
fi local bar="\${CYAN}\${hash:0:filled}\${NC}\${dash:0:empty}"
local spin_char="\${spinstr:$spin_idx:1}"
printf "\\r\\033[K[\${CYAN}" spin_idx=$(( (spin_idx + 1) % 4 ))
printf "%\${filled}s" | tr ' ' '█'
printf "\${NC}" printf "\\r\\033[K[%b] %3d%% (%d/%d) \${BOLD}%s\${NC} [%c] %ds" "$bar" "$percent" "$CURRENT" "$TOTAL" "$name" "$spin_char" "$elapsed" >&3
printf "%\${empty}s" | tr ' ' '░'
printf "] %3d%% (%d/%d) \${BOLD}%s\${NC} \${DIM}%s left\${NC}" "$percent" "$current" "$total" "$name" "$eta_str" 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() { with_retry() {
local max_attempts=3 local attempt=1 max=3 delay=5
local attempt=1 while [ $attempt -le $max ]; do
local delay=5 echo "=== Executing (Attempt $attempt/$max): $* ==="
if "$@"; then return 0; fi
while [ $attempt -le $max_attempts ]; do echo "=== Command failed ==="
if output=$(run_cmd "$@"); then if [ $attempt -lt $max ]; then
echo "$output" echo "Retrying in \${delay}s..."
return 0 sleep $delay
delay=$((delay * 2))
fi fi
attempt=$((attempt + 1))
# 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
done done
return 1 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() { print_summary() {
local end_time=$(date +%s) local end_time=$(date +%s)
local duration=$((end_time - START_TIME)) local duration=$((end_time - START_TIME))
local mins=$((duration / 60)) local mins=$((duration / 60))
local secs=$((duration % 60)) local secs=$((duration % 60))
echo echo >&3
echo "─────────────────────────────────────────────────────────────────────────────" echo "---------------------------------------------------------------------------" >&3
local installed=\${#SUCCEEDED[@]} local installed=\${#SUCCEEDED[@]}
local skipped_count=\${#SKIPPED[@]} local skipped_count=\${#SKIPPED[@]}
local failed_count=\${#FAILED[@]} local failed_count=\${#FAILED[@]}
if [ $failed_count -eq 0 ]; then if [ $failed_count -eq 0 ]; then
if [ $skipped_count -gt 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 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 fi
else else
echo -e "\${YELLOW}!\${NC} $installed installed, $skipped_count skipped, $failed_count failed \${DIM}(\${mins}m \${secs}s)\${NC}" echo -e "\${YELLOW}[!]\${NC} $installed installed, $skipped_count skipped, $failed_count failed \${DIM}(\${mins}m \${secs}s)\${NC}" >&3
echo echo >&3
echo -e "\${RED}Failed:\${NC}" echo -e "\${RED}Failed:\${NC}" >&3
for pkg in "\${FAILED[@]}"; do for pkg in "\${FAILED[@]}"; do
echo " $pkg" echo " - $pkg" >&3
done done
fi fi
echo "─────────────────────────────────────────────────────────────────────────────" echo "---------------------------------------------------------------------------" >&3
echo -e "\${DIM}Log: $LOG\${NC}" >&3
} }
`; `;

View File

@@ -1,62 +1,65 @@
// Snap script
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateSnapScript(packages: PackageInfo[]): string { 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() { 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 " snap list 2>/dev/null | grep -q "^$snap_name "
} }
install_pkg() { install_pkg() {
local name=$1 pkg=$2 local name=$1 pkg=$2
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$pkg"; then if is_installed "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
local output # pkg is intentionally unquoted: it may contain flags like --classic
if output=$(with_retry sudo snap install $pkg); then with_retry sudo snap install $pkg &
local pid=$!
if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
if echo "$output" | grep -q "not found"; then error "$name"
echo -e " \${DIM}Snap not found\${NC}"
fi
FAILED+=("$name") FAILED+=("$name")
fi fi
} }
# ───────────────────────────────────────────────────────────────────────────── # ---------------------------------------------------------------------------
command -v snap &>/dev/null || { command -v snap &>/dev/null || {
error "Snap not installed" error "Snap not installed"
info "Install: sudo apt/dnf/pacman install snapd" info "Install: sudo apt/dnf/pacman install snapd" >&3
exit 1 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 if command -v systemctl &>/dev/null && ! systemctl is-active --quiet snapd; then
info "Starting snapd..." info "Starting snapd..."
sudo systemctl enable --now snapd.socket sudo systemctl enable --now snapd.socket >/dev/null 2>&1
sudo systemctl start snapd sudo systemctl start snapd >/dev/null 2>&1
sleep 2 sleep 2
printf "\\r\\033[K" >&3
success "snapd started" success "snapd started"
fi fi
echo echo >&3
info "Installing $TOTAL packages" info "Installing $TOTAL packages"
echo echo >&3
${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')}

View File

@@ -1,81 +1,74 @@
// Ubuntu script - apt-get with dependency auto-fix
import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared'; import { generateAsciiHeader, generateSharedUtils, escapeShellString, type PackageInfo } from './shared';
export function generateUbuntuScript(packages: PackageInfo[]): string { export function generateUbuntuScript(packages: PackageInfo[]): string {
return generateAsciiHeader('Ubuntu', packages.length) + generateSharedUtils(packages.length) + ` return generateAsciiHeader('Ubuntu', packages.length) + generateSharedUtils('ubuntu', packages.length) + `
is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } export DEBIAN_FRONTEND=noninteractive
# Auto-fix broken dependencies is_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; }
fix_deps() {
if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then
success "Dependencies fixed"
return 0
fi
return 1
}
install_pkg() { install_pkg() {
local name=$1 pkg=$2 local name=$1 pkg=$2
CURRENT=$((CURRENT + 1)) CURRENT=$((CURRENT + 1))
if is_installed "$pkg"; then if is_installed "$pkg"; then
skip "$name" skip "$name"
SKIPPED+=("$name") SKIPPED+=("$name")
return 0 return 0
fi fi
show_progress $CURRENT $TOTAL "$name"
local start=$(date +%s) local start=$(date +%s)
local output with_retry sudo apt-get install -y "$pkg" &
if output=$(with_retry sudo apt-get install -y "$pkg"); then local pid=$!
if animate_progress "$name" $pid; then
local elapsed=$(($(date +%s) - start)) local elapsed=$(($(date +%s) - start))
update_avg_time $elapsed printf "\\r\\033[K" >&3
printf "\\r\\033[K" success "$name" "\${elapsed}s"
timing "$name" "$elapsed"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
else else
printf "\\r\\033[K\${RED}\${NC} %s\\n" "$name" printf "\\r\\033[K" >&3
if echo "$output" | grep -q "Unable to locate"; then if tail -n 50 "$LOG" | grep -q "unmet dependencies"; then
echo -e " \${DIM}Package not found\${NC}" warn "Fixing dependencies for $name..." >&3
elif echo "$output" | grep -q "unmet dependencies"; then if sudo apt-get --fix-broken install -y >/dev/null 2>&1; then
echo -e " \${DIM}Fixing dependencies...\${NC}" sudo apt-get install -y "$pkg" &
if fix_deps; then if animate_progress "$name (retry)" $!; then
# Retry once after fixing deps local elapsed=$(($(date +%s) - start))
if sudo apt-get install -y "$pkg" >/dev/null 2>&1; then success "$name" "\${elapsed}s, deps fixed"
timing "$name" "$(($(date +%s) - start))"
SUCCEEDED+=("$name") SUCCEEDED+=("$name")
return 0 return 0
fi fi
fi fi
fi fi
error "$name"
FAILED+=("$name") FAILED+=("$name")
fi 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 info "Caching sudo credentials..."
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sudo -v || exit 1
warn "Waiting for package manager..." while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
sleep 2
done wait_for_lock /var/lib/dpkg/lock-frontend
sudo dpkg --configure -a >/dev/null 2>&1 || true
info "Updating package lists..." 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 >&3
# Installation
# ─────────────────────────────────────────────────────────────────────────────
echo
info "Installing $TOTAL packages" info "Installing $TOTAL packages"
echo echo >&3
${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')} ${packages.map(({ app, pkg }) => `install_pkg "${escapeShellString(app.name)}" "${pkg}"`).join('\n')}

View File

@@ -1,8 +1,8 @@
{ {
"meta": { "meta": {
"fetchedAt": "2026-02-22T09:54:12.215Z" "fetchedAt": "2026-02-23T09:27:17.164Z"
}, },
"count": 697, "count": 695,
"apps": [ "apps": [
"ai.jan.Jan", "ai.jan.Jan",
"app.bluebubbles.BlueBubbles", "app.bluebubbles.BlueBubbles",
@@ -225,7 +225,6 @@
"fr.handbrake.ghb", "fr.handbrake.ghb",
"garden.jamie.Morphosis", "garden.jamie.Morphosis",
"gg.minion.Minion", "gg.minion.Minion",
"gg.norisk.NoRiskClientLauncherV3",
"hu.irl.cameractrls", "hu.irl.cameractrls",
"im.bernard.Nostalgia", "im.bernard.Nostalgia",
"im.dino.Dino", "im.dino.Dino",
@@ -342,7 +341,6 @@
"io.github.nozwock.Packet", "io.github.nozwock.Packet",
"io.github.nroduit.Weasis", "io.github.nroduit.Weasis",
"io.github.nuttyartist.notes", "io.github.nuttyartist.notes",
"io.github.olaproeis.Ferrite",
"io.github.onionware_github.onionmedia", "io.github.onionware_github.onionmedia",
"io.github.pantheon_tweaks.pantheon-tweaks", "io.github.pantheon_tweaks.pantheon-tweaks",
"io.github.peazip.PeaZip", "io.github.peazip.PeaZip",
@@ -399,7 +397,6 @@
"io.gitlab.theevilskeleton.Upscaler", "io.gitlab.theevilskeleton.Upscaler",
"io.kapsa.drive", "io.kapsa.drive",
"io.kinvolk.Headlamp", "io.kinvolk.Headlamp",
"io.m51.Gelly",
"io.missioncenter.MissionCenter", "io.missioncenter.MissionCenter",
"io.openrct2.OpenRCT2", "io.openrct2.OpenRCT2",
"io.podman_desktop.PodmanDesktop", "io.podman_desktop.PodmanDesktop",
@@ -456,6 +453,7 @@
"org.altlinux.Tuner", "org.altlinux.Tuner",
"org.azahar_emu.Azahar", "org.azahar_emu.Azahar",
"org.bunkus.mkvtoolnix-gui", "org.bunkus.mkvtoolnix-gui",
"org.chessmd.chessmd",
"org.cloudcompare.CloudCompare", "org.cloudcompare.CloudCompare",
"org.cockpit_project.CockpitClient", "org.cockpit_project.CockpitClient",
"org.contourterminal.Contour", "org.contourterminal.Contour",
@@ -630,7 +628,6 @@
"org.openrgb.OpenRGB", "org.openrgb.OpenRGB",
"org.openscad.OpenSCAD", "org.openscad.OpenSCAD",
"org.opensurge2d.OpenSurge", "org.opensurge2d.OpenSurge",
"org.pencil2d.Pencil2D",
"org.photoqt.PhotoQt", "org.photoqt.PhotoQt",
"org.pitivi.Pitivi", "org.pitivi.Pitivi",
"org.ppsspp.PPSSPP", "org.ppsspp.PPSSPP",
@@ -673,6 +670,7 @@
"page.codeberg.libre_menu_editor.LibreMenuEditor", "page.codeberg.libre_menu_editor.LibreMenuEditor",
"page.codeberg.lo_vely.Nucleus", "page.codeberg.lo_vely.Nucleus",
"page.codeberg.tahoso.azul-box", "page.codeberg.tahoso.azul-box",
"page.codeberg.vendillah.GamepadMirror",
"page.kramo.Cartridges", "page.kramo.Cartridges",
"page.tesk.Refine", "page.tesk.Refine",
"re.fossplant.songrec", "re.fossplant.songrec",