Skip to content

Instantly share code, notes, and snippets.

@EdwardKerckhof
Last active March 12, 2026 22:27
Show Gist options
  • Select an option

  • Save EdwardKerckhof/0f0bee87cfc82f0073e1dd7f63c3c437 to your computer and use it in GitHub Desktop.

Select an option

Save EdwardKerckhof/0f0bee87cfc82f0073e1dd7f63c3c437 to your computer and use it in GitHub Desktop.
Homelab CLI installer
# Install the homelab CLI from the private GitHub repo.
#
# Usage (remote):
# gh api repos/EdwardKerckhof/homelab-v2/contents/cli/install.ps1 -H "Accept: application/vnd.github.raw" | iex
#
# Usage (local, from a cloned repo):
# .\cli\install.ps1
#
# On a fresh Windows machine, this script auto-installs git, python, and pipx
# via winget before installing the CLI.
param(
[string]$Version = "main"
)
$ErrorActionPreference = "Stop"
$Repo = "EdwardKerckhof/homelab-v2"
$HttpsUrl = "git+https://github.com/$Repo.git@$Version#subdirectory=cli"
$SshUrl = "git+ssh://git@github.com/$Repo.git@$Version#subdirectory=cli"
$PkgSpec = "homelab-cli @ $HttpsUrl"
$PkgSpecSsh = "homelab-cli @ $SshUrl"
# ── Helpers ───────────────────────────────────────────────────────────────────
function Has-Winget {
return [bool](Get-Command winget -ErrorAction SilentlyContinue)
}
function Winget-Install {
param([string]$PackageId, [string]$Name)
if (-not (Has-Winget)) {
throw "$Name is required. Install it manually or install winget first."
}
Write-Host "==> Installing $Name via winget..."
winget install --id $PackageId --accept-source-agreements --accept-package-agreements --silent
# Refresh PATH for current session
$machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$env:Path = "$machinePath;$userPath"
}
# ── Ensure git ────────────────────────────────────────────────────────────────
function Ensure-Git {
if (Get-Command git -ErrorAction SilentlyContinue) { return }
Winget-Install "Git.Git" "Git"
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
throw "Git installed but not on PATH. Restart your terminal and re-run."
}
}
# ── Ensure Python 3.10+ ──────────────────────────────────────────────────────
function Find-Python {
foreach ($cmd in @("python3", "python", "py")) {
if (Get-Command $cmd -ErrorAction SilentlyContinue) {
$ver = & $cmd -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>$null
if ($ver) {
$parts = $ver -split '\.'
if ([int]$parts[0] -ge 3 -and [int]$parts[1] -ge 10) {
return $cmd
}
}
}
}
return $null
}
function Ensure-Python {
$script:Python = Find-Python
if ($script:Python) { return }
Winget-Install "Python.Python.3.13" "Python 3.13"
$script:Python = Find-Python
if (-not $script:Python) {
throw "Python 3.10+ installed but not on PATH. Restart your terminal and re-run."
}
}
# ── Ensure GitHub CLI ─────────────────────────────────────────────────────────
function Ensure-Gh {
if (Get-Command gh -ErrorAction SilentlyContinue) { return }
Write-Host "==> GitHub CLI (gh) is required to access the private repo."
Winget-Install "GitHub.cli" "GitHub CLI"
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
throw "gh installed but not on PATH. Restart your terminal and re-run."
}
}
function Ensure-GhAuth {
$authStatus = gh auth status 2>&1
if ($LASTEXITCODE -eq 0) { return }
Write-Host ""
Write-Host " You need to log in to GitHub so the installer can access the private repo."
Write-Host ""
gh auth login
$authCheck = gh auth status 2>&1
if ($LASTEXITCODE -ne 0) { throw "GitHub authentication failed." }
}
# ── Ensure pipx ──────────────────────────────────────────────────────────────
function Ensure-Pipx {
if (Get-Command pipx -ErrorAction SilentlyContinue) { return }
Write-Host "==> Installing pipx..."
& $script:Python -m pip install --user pipx
& $script:Python -m pipx ensurepath
# Refresh PATH for current session
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$env:Path = "$userPath;$env:Path"
if (-not (Get-Command pipx -ErrorAction SilentlyContinue)) {
throw "pipx installed but not on PATH. Restart your terminal and re-run."
}
}
# ── Pre-flight ────────────────────────────────────────────────────────────────
Write-Host "==> Homelab CLI installer"
Ensure-Git
Ensure-Python
Write-Host " Python: $Python ($(& $Python --version 2>&1))"
Ensure-Gh
Ensure-GhAuth
# Configure git to use gh for HTTPS auth (pipx install clones via git)
gh auth setup-git
# ── Install or upgrade ────────────────────────────────────────────────────────
Ensure-Pipx
$pipxList = pipx list --short 2>$null
if ($pipxList -match '(?m)^homelab-cli ') {
Write-Host "==> homelab-cli already installed - upgrading..."
pipx upgrade homelab-cli
} else {
Write-Host "==> Installing homelab CLI with pipx..."
try {
pipx install $PkgSpec
} catch {
Write-Host "==> HTTPS install failed - trying SSH..."
pipx install $PkgSpecSsh
}
}
Write-Host ""
Write-Host "Done! Run 'homelab' to get started."
#!/usr/bin/env bash
# Install the homelab CLI from the private GitHub repo.
#
# Usage (remote — pick one):
# bash <(gh api repos/EdwardKerckhof/homelab-v2/contents/cli/install.sh -H "Accept: application/vnd.github.raw")
# curl -fsSL -H "Authorization: token $GITHUB_TOKEN" https://raw.githubusercontent.com/EdwardKerckhof/homelab-v2/main/cli/install.sh | bash
#
# Usage (local, from a cloned repo):
# bash cli/install.sh
#
# On a fresh Ubuntu/Debian or macOS, this script auto-installs git, python3,
# and pipx before installing the CLI.
set -euo pipefail
# ── Parse arguments ───────────────────────────────────────────────────────────
VERSION="main"
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
REPO="EdwardKerckhof/homelab-v2"
HTTPS_URL="git+https://github.com/${REPO}.git@${VERSION}#subdirectory=cli"
SSH_URL="git+ssh://git@github.com/${REPO}.git@${VERSION}#subdirectory=cli"
PKG_SPEC="homelab-cli @ ${HTTPS_URL}"
PKG_SPEC_SSH="homelab-cli @ ${SSH_URL}"
# ── Helpers ───────────────────────────────────────────────────────────────────
die() { echo "Error: $*" >&2; exit 1; }
# Detect package manager
detect_pkg_manager() {
if command -v apt-get &>/dev/null; then
PKG_MGR="apt"
elif command -v dnf &>/dev/null; then
PKG_MGR="dnf"
elif command -v brew &>/dev/null; then
PKG_MGR="brew"
else
PKG_MGR=""
fi
}
# Run with sudo if needed (skips if already root)
maybe_sudo() {
if [[ $EUID -eq 0 ]]; then
"$@"
elif command -v sudo &>/dev/null; then
sudo "$@"
else
die "Need root privileges to install packages. Run as root or install sudo."
fi
}
pkg_install() {
local pkg="$1"
echo "==> Installing $pkg..."
case "$PKG_MGR" in
apt) maybe_sudo apt-get update -qq && maybe_sudo apt-get install -y -qq "$pkg" ;;
dnf) maybe_sudo dnf install -y -q "$pkg" ;;
brew) brew install "$pkg" ;;
*) die "No supported package manager found. Install $pkg manually." ;;
esac
}
# ── Ensure git ────────────────────────────────────────────────────────────────
ensure_git() {
if command -v git &>/dev/null; then return; fi
pkg_install git
command -v git &>/dev/null || die "Failed to install git."
}
# ── Ensure Python 3.10+ ──────────────────────────────────────────────────────
ensure_python() {
for cmd in python3 python; do
if command -v "$cmd" &>/dev/null; then
local ver
ver=$("$cmd" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
local major=${ver%%.*} minor=${ver##*.}
if (( major >= 3 && minor >= 10 )); then
PYTHON="$cmd"
return
fi
fi
done
# Auto-install
case "$PKG_MGR" in
apt) pkg_install python3 && pkg_install python3-venv ;;
dnf) pkg_install python3 ;;
brew) pkg_install python@3 ;;
*) die "Python 3.10+ is required. Install it manually." ;;
esac
# Re-check after install
for cmd in python3 python; do
if command -v "$cmd" &>/dev/null; then
local ver
ver=$("$cmd" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
local major=${ver%%.*} minor=${ver##*.}
if (( major >= 3 && minor >= 10 )); then
PYTHON="$cmd"
return
fi
fi
done
die "Python 3.10+ is required but could not be installed."
}
# ── Ensure GitHub CLI ─────────────────────────────────────────────────────────
ensure_gh() {
if command -v gh &>/dev/null; then return; fi
echo "==> GitHub CLI (gh) is required to access the private repo."
case "$PKG_MGR" in
apt)
# Official gh repo for Debian/Ubuntu
maybe_sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| maybe_sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| maybe_sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null
maybe_sudo apt-get update -qq && maybe_sudo apt-get install -y -qq gh
;;
dnf) pkg_install gh ;;
brew) brew install gh ;;
*) die "Install the GitHub CLI manually: https://cli.github.com" ;;
esac
command -v gh &>/dev/null || die "Failed to install gh."
}
ensure_gh_auth() {
if gh auth status &>/dev/null; then return; fi
echo ""
echo " You need to log in to GitHub so the installer can access the private repo."
echo ""
gh auth login
gh auth status &>/dev/null || die "GitHub authentication failed."
}
# ── Ensure pipx ──────────────────────────────────────────────────────────────
ensure_pipx() {
if command -v pipx &>/dev/null; then return; fi
echo "==> Installing pipx..."
# On Debian/Ubuntu, prefer the system package (avoids PEP 668)
if [[ "$PKG_MGR" == "apt" ]]; then
pkg_install pipx
elif [[ "$PKG_MGR" == "dnf" ]]; then
pkg_install pipx
elif [[ "$PKG_MGR" == "brew" ]]; then
brew install pipx
else
"$PYTHON" -m pip install --user pipx --break-system-packages 2>/dev/null \
|| "$PYTHON" -m pip install --user pipx \
|| die "Failed to install pipx."
fi
"$PYTHON" -m pipx ensurepath 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
command -v pipx &>/dev/null || die "pipx installed but not on PATH. Restart your terminal and re-run."
}
# ── Pre-flight ────────────────────────────────────────────────────────────────
echo "==> Homelab CLI installer"
detect_pkg_manager
ensure_git
ensure_python
echo " Python: $PYTHON ($($PYTHON --version 2>&1))"
ensure_gh
ensure_gh_auth
# Configure git to use gh for HTTPS auth (pipx install clones via git)
gh auth setup-git
# ── Install or upgrade ────────────────────────────────────────────────────────
ensure_pipx
if pipx list --short 2>/dev/null | grep -q '^homelab-cli '; then
echo "==> homelab-cli already installed — upgrading..."
pipx upgrade homelab-cli || true
else
echo "==> Installing homelab CLI with pipx..."
if ! pipx install "$PKG_SPEC"; then
echo "==> HTTPS install failed — trying SSH..."
pipx install "$PKG_SPEC_SSH"
fi
fi
echo ""
echo "Done! Run 'homelab' to get started."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment