From 7ca223c6b2832802c86ca0879c4c3a6169003b8c Mon Sep 17 00:00:00 2001
From: fllipeis
Date: Sat, 22 Aug 2026 18:26:17 +0200
Subject: [PATCH] feat: add CLI installers
---
.github/workflows/test-cli-installer.yml | 48 ++++++
README.md | 18 ++-
docs/getting_started.md | 17 ++-
scripts/install.ps1 | 167 ++++++++++++++++++++
scripts/install.sh | 185 +++++++++++++++++++++++
5 files changed, 428 insertions(+), 7 deletions(-)
create mode 100644 .github/workflows/test-cli-installer.yml
create mode 100644 scripts/install.ps1
create mode 100755 scripts/install.sh
diff --git a/.github/workflows/test-cli-installer.yml b/.github/workflows/test-cli-installer.yml
new file mode 100644
index 00000000..be6ce44c
--- /dev/null
+++ b/.github/workflows/test-cli-installer.yml
@@ -0,0 +1,48 @@
+name: test-cli-installer
+
+on:
+ push:
+ paths:
+ - "scripts/install.sh"
+ - "scripts/install.ps1"
+ - ".github/workflows/test-cli-installer.yml"
+ pull_request:
+ paths:
+ - "scripts/install.sh"
+ - "scripts/install.ps1"
+ - ".github/workflows/test-cli-installer.yml"
+
+permissions:
+ contents: read
+
+jobs:
+ unix:
+ strategy:
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v7
+ - name: install the latest Explorer CLI
+ env:
+ EXPLORER_INSTALL_DIR: ${{ runner.temp }}/explorer-bin
+ NO_COLOR: 1
+ run: ./scripts/install.sh
+ - name: run the installed CLI
+ run: $RUNNER_TEMP/explorer-bin/explorer version
+
+ windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v7
+ - name: install the latest Explorer CLI
+ shell: pwsh
+ env:
+ EXPLORER_INSTALL_DIR: ${{ runner.temp }}\explorer-bin
+ NO_COLOR: 1
+ run: ./scripts/install.ps1
+ - name: run the installed CLI
+ shell: pwsh
+ run: '& "$env:RUNNER_TEMP\explorer-bin\explorer.exe" version'
diff --git a/README.md b/README.md
index 9a7ece5a..a6ea139b 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,21 @@ If there is a concrete roadmap it will be posted on our [Discord](http://discord
**Limitations**
* Only single-platform OCI images can be built, so setups containing both `linux/arm64` and `linux/amd64` hosts is not possible as of now.
-## License
-This project uses two different licenses: AGPLv3 and LGPLv3. Everything found under under the `api/` folder is licensed under LGPLv3 while everything else is covered by AGPLv3, if not stated otherwise.
+## Install the CLI
+
+On macOS or Linux:
+
+```sh
+curl -fsSL https://raw.githubusercontent.com/spacechunks/explorer/main/scripts/install.sh | sh
+```
+On Windows PowerShell:
+```powershell
+irm https://raw.githubusercontent.com/spacechunks/explorer/main/scripts/install.ps1 | iex
+```
+
+This installs the latest version of the CLI.
+
+## License
+This project uses two different licenses: AGPLv3 and LGPLv3. Everything found under under the `api/` folder is licensed under LGPLv3 while everything else is covered by AGPLv3, if not stated otherwise.
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 3ce9196e..0000bce3 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -16,12 +16,19 @@ Here, we will cover the basics on how to get started with the Chunk Explorer. Ha
## CLI installation
-In order to interact with the Explorer, you need our CLI tool. To install it head over to the [releases page](https://github.com/spacechunks/explorer/releases),
-and download the binary that is compatible with your system. The CLI is available for the following operating systems and architectures:
+On macOS or Linux:
-- Linux (arm64/amd64)
-- MacOS (arm64/amd64)
-- Windows (arm64/amd64)
+```sh
+curl -fsSL https://raw.githubusercontent.com/spacechunks/explorer/main/scripts/install.sh | sh
+```
+
+On Windows PowerShell:
+
+```powershell
+irm https://raw.githubusercontent.com/spacechunks/explorer/main/scripts/install.ps1 | iex
+```
+
+The installer downloads the latest version of the CLI.
## Concepts
diff --git a/scripts/install.ps1 b/scripts/install.ps1
new file mode 100644
index 00000000..f4464e9c
--- /dev/null
+++ b/scripts/install.ps1
@@ -0,0 +1,167 @@
+& {
+ Set-StrictMode -Version Latest
+ $ErrorActionPreference = "Stop"
+
+ $repositoryUrl = "https://github.com/spacechunks/explorer"
+ $binaryName = "explorer.exe"
+ $tempDirectory = $null
+
+ function Write-Brand {
+ Write-Host ""
+ if ($env:NO_COLOR) {
+ Write-Host "✦" -NoNewline
+ } else {
+ Write-Host "✦" -ForegroundColor Magenta -NoNewline
+ }
+ Write-Host " Chunk Explorer"
+ Write-Host ""
+ }
+
+ function Write-Step([string] $Message) {
+ Write-Host " " -NoNewline
+ if ($env:NO_COLOR) {
+ Write-Host "✓" -NoNewline
+ } else {
+ Write-Host "✓" -ForegroundColor Green -NoNewline
+ }
+ Write-Host " $Message"
+ }
+
+ function Get-LatestVersion {
+ $response = Invoke-WebRequest -Uri "$repositoryUrl/releases/latest" -Method Head -UseBasicParsing
+ if ($response.BaseResponse.PSObject.Properties.Name -contains "ResponseUri") {
+ $releaseUri = $response.BaseResponse.ResponseUri
+ } elseif ($response.BaseResponse.PSObject.Properties.Name -contains "RequestMessage") {
+ $releaseUri = $response.BaseResponse.RequestMessage.RequestUri
+ } else {
+ throw "GitHub did not return the latest release URL."
+ }
+
+ return [Uri]::UnescapeDataString($releaseUri.Segments[-1].TrimEnd("/"))
+ }
+
+ try {
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor `
+ [Net.SecurityProtocolType]::Tls12
+
+ $architectureName = if ($env:PROCESSOR_ARCHITEW6432) {
+ $env:PROCESSOR_ARCHITEW6432
+ } else {
+ $env:PROCESSOR_ARCHITECTURE
+ }
+
+ $architecture = switch ($architectureName.ToUpperInvariant()) {
+ "AMD64" { "amd64" }
+ "ARM64" { "arm64" }
+ default { throw "Unsupported architecture: $architectureName" }
+ }
+
+ $version = $env:EXPLORER_VERSION
+ if ([string]::IsNullOrWhiteSpace($version)) {
+ $version = Get-LatestVersion
+ }
+ if (-not $version.StartsWith("v")) {
+ $version = "v$version"
+ }
+ if ($version -notmatch '^v[A-Za-z0-9._+\-]+$') {
+ throw "Invalid Explorer CLI version: $version"
+ }
+
+ $installDirectory = $env:EXPLORER_INSTALL_DIR
+ if ([string]::IsNullOrWhiteSpace($installDirectory)) {
+ $installDirectory = Join-Path $env:LOCALAPPDATA "Programs\Explorer"
+ }
+ $installDirectory = $installDirectory.TrimEnd("\", "/")
+ if ([string]::IsNullOrWhiteSpace($installDirectory)) {
+ throw "The installation directory cannot be empty."
+ }
+
+ $archiveName = "explorer_${version}_windows_${architecture}.tar.gz"
+ $checksumName = "explorer_${version}_sha256sums"
+ $releaseBinary = "explorer_${version}_windows_${architecture}.exe"
+ $escapedVersion = [Uri]::EscapeDataString($version)
+ $releaseUrl = "$repositoryUrl/releases/download/$escapedVersion"
+
+ $tempDirectory = Join-Path ([IO.Path]::GetTempPath()) "explorer-install-$([Guid]::NewGuid())"
+ New-Item -ItemType Directory -Path $tempDirectory | Out-Null
+ $archivePath = Join-Path $tempDirectory $archiveName
+ $checksumPath = Join-Path $tempDirectory $checksumName
+
+ Write-Brand
+ Write-Host " Installing $version for Windows $architecture"
+ Write-Host ""
+
+ Invoke-WebRequest -Uri "$releaseUrl/$archiveName" -OutFile $archivePath -UseBasicParsing
+ Invoke-WebRequest -Uri "$releaseUrl/$checksumName" -OutFile $checksumPath -UseBasicParsing
+ Write-Step "Downloaded $archiveName"
+
+ $escapedArchiveName = [Regex]::Escape($archiveName)
+ $checksumLine = Get-Content -LiteralPath $checksumPath |
+ Where-Object { $_ -match "^([a-fA-F0-9]{64})\s+$escapedArchiveName$" } |
+ Select-Object -First 1
+ if ($null -eq $checksumLine) {
+ throw "The checksum manifest does not contain $archiveName."
+ }
+
+ $expectedChecksum = ($checksumLine -split '\s+')[0]
+ $actualChecksum = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash
+ if ($actualChecksum -ine $expectedChecksum) {
+ throw "Checksum verification failed for $archiveName."
+ }
+ Write-Step "Verified the SHA-256 checksum"
+
+ $tar = Get-Command "tar.exe" -ErrorAction SilentlyContinue
+ if ($null -eq $tar) {
+ throw "tar.exe is required to extract the Explorer CLI release."
+ }
+ & $tar.Source -xzf $archivePath -C $tempDirectory
+ if ($LASTEXITCODE -ne 0) {
+ throw "Could not extract $archiveName."
+ }
+
+ $sourceBinary = Join-Path $tempDirectory $releaseBinary
+ if (-not (Test-Path -LiteralPath $sourceBinary -PathType Leaf)) {
+ throw "The release archive does not contain $releaseBinary."
+ }
+
+ New-Item -ItemType Directory -Path $installDirectory -Force | Out-Null
+ $installedBinary = Join-Path $installDirectory $binaryName
+ Copy-Item -LiteralPath $sourceBinary -Destination $installedBinary -Force
+ Write-Step "Installed explorer to $installedBinary"
+
+ $pathEntries = @($env:Path -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
+ if (-not ($pathEntries | Where-Object { $_.TrimEnd("\") -ieq $installDirectory.TrimEnd("\") })) {
+ $env:Path = "$installDirectory;$env:Path"
+
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
+ $userPathEntries = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
+ if (-not ($userPathEntries | Where-Object { $_.TrimEnd("\") -ieq $installDirectory.TrimEnd("\") })) {
+ $newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) {
+ $installDirectory
+ } else {
+ "$installDirectory;$userPath"
+ }
+ [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User")
+ }
+ }
+
+ Write-Host ""
+ Write-Host "Explorer CLI is ready."
+ Write-Host "Run " -NoNewline
+ Write-Host "explorer --help" -NoNewline
+ Write-Host " to get started."
+ Write-Host ""
+ } catch {
+ if ($env:NO_COLOR) {
+ Write-Host "`n Error: $($_.Exception.Message)`n" -ErrorAction Continue
+ } else {
+ Write-Host "`n Error: " -ForegroundColor Red -NoNewline
+ Write-Host "$($_.Exception.Message)`n"
+ }
+ throw
+ } finally {
+ if ($null -ne $tempDirectory -and (Test-Path -LiteralPath $tempDirectory)) {
+ Remove-Item -LiteralPath $tempDirectory -Recurse -Force
+ }
+ }
+}
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100755
index 00000000..4a58b7e5
--- /dev/null
+++ b/scripts/install.sh
@@ -0,0 +1,185 @@
+#!/bin/sh
+
+set -eu
+
+repository_url="https://github.com/spacechunks/explorer"
+binary_name="explorer"
+temp_dir=""
+install_temp=""
+
+if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ escape="$(printf '\033')"
+ bold="${escape}[1m"
+ dim="${escape}[2m"
+ green="${escape}[32m"
+ purple="${escape}[35m"
+ red="${escape}[31m"
+ reset="${escape}[0m"
+else
+ bold=""
+ dim=""
+ green=""
+ purple=""
+ red=""
+ reset=""
+fi
+
+cleanup() {
+ if [ -n "$install_temp" ] && [ -f "$install_temp" ]; then
+ rm -f "$install_temp"
+ fi
+ if [ -n "$temp_dir" ] && [ -d "$temp_dir" ]; then
+ rm -rf "$temp_dir"
+ fi
+}
+
+fail() {
+ printf '\n %sError:%s %s\n\n' "$red" "$reset" "$*" >&2
+ exit 1
+}
+
+step() {
+ printf ' %s✓%s %s\n' "$green" "$reset" "$1"
+}
+
+download() {
+ curl --fail --silent --show-error --location --retry 3 \
+ --connect-timeout 10 "$1" --output "$2"
+}
+
+sha256() {
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$1" | awk '{ print $1 }'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | awk '{ print $1 }'
+ elif command -v openssl >/dev/null 2>&1; then
+ openssl dgst -sha256 "$1" | awk '{ print $NF }'
+ else
+ return 1
+ fi
+}
+
+trap cleanup EXIT
+trap 'exit 1' HUP INT TERM
+
+command -v curl >/dev/null 2>&1 || fail "curl is required to install the Explorer CLI."
+command -v tar >/dev/null 2>&1 || fail "tar is required to install the Explorer CLI."
+
+kernel="$(uname -s)"
+machine="$(uname -m)"
+
+case "$kernel" in
+ Darwin)
+ operating_system="darwin"
+ operating_system_label="macOS"
+ binary_extension=""
+ ;;
+ Linux)
+ operating_system="linux"
+ operating_system_label="Linux"
+ binary_extension=""
+ ;;
+ MINGW* | MSYS* | CYGWIN*)
+ operating_system="windows"
+ operating_system_label="Windows"
+ binary_extension=".exe"
+ ;;
+ *)
+ fail "Unsupported operating system: $kernel"
+ ;;
+esac
+
+case "$machine" in
+ x86_64 | amd64 | AMD64)
+ architecture="amd64"
+ ;;
+ aarch64 | arm64 | ARM64)
+ architecture="arm64"
+ ;;
+ *)
+ fail "Unsupported architecture: $machine"
+ ;;
+esac
+
+version="${EXPLORER_VERSION:-}"
+if [ -z "$version" ]; then
+ latest_url="$(curl --fail --silent --show-error --location --retry 3 \
+ --connect-timeout 10 --output /dev/null --write-out '%{url_effective}' \
+ "$repository_url/releases/latest")" || fail "Could not determine the latest Explorer CLI version."
+ version="${latest_url##*/}"
+ version="$(printf '%s' "$version" | sed 's/%2[Bb]/+/g')"
+fi
+
+case "$version" in
+ v*) ;;
+ *) version="v$version" ;;
+esac
+
+case "$version" in
+ *[!A-Za-z0-9._+-]*) fail "Invalid Explorer CLI version: $version" ;;
+esac
+
+if [ -n "${EXPLORER_INSTALL_DIR:-}" ]; then
+ install_dir="${EXPLORER_INSTALL_DIR%/}"
+elif [ -n "${XDG_BIN_HOME:-}" ]; then
+ install_dir="${XDG_BIN_HOME%/}"
+elif [ -n "${HOME:-}" ]; then
+ install_dir="$HOME/.local/bin"
+else
+ fail "HOME is not set. Set EXPLORER_INSTALL_DIR to choose an installation directory."
+fi
+
+[ -n "$install_dir" ] || fail "The installation directory cannot be empty."
+
+archive_name="explorer_${version}_${operating_system}_${architecture}.tar.gz"
+checksum_name="explorer_${version}_sha256sums"
+release_binary="explorer_${version}_${operating_system}_${architecture}${binary_extension}"
+release_url="$repository_url/releases/download/$version"
+
+temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/explorer-install.XXXXXX")" || \
+ fail "Could not create a temporary directory."
+archive_path="$temp_dir/$archive_name"
+checksum_path="$temp_dir/$checksum_name"
+
+printf '\n%s✦%s %sChunk Explorer%s\n\n' "$purple" "$reset" "$bold" "$reset"
+printf ' %sInstalling %s for %s %s%s\n\n' \
+ "$dim" "$version" "$operating_system_label" "$architecture" "$reset"
+
+download "$release_url/$archive_name" "$archive_path" || \
+ fail "Could not download $archive_name. Check that the release supports $operating_system/$architecture."
+download "$release_url/$checksum_name" "$checksum_path" || \
+ fail "Could not download the checksum manifest for $version."
+step "Downloaded $archive_name"
+
+expected_checksum="$(awk -v name="$archive_name" '$2 == name { print $1; exit }' "$checksum_path")"
+[ -n "$expected_checksum" ] || fail "The checksum manifest does not contain $archive_name."
+actual_checksum="$(sha256 "$archive_path")" || \
+ fail "A SHA-256 tool is required. Install sha256sum, shasum, or openssl."
+
+[ "$actual_checksum" = "$expected_checksum" ] || \
+ fail "Checksum verification failed for $archive_name."
+step "Verified the SHA-256 checksum"
+
+tar -xzf "$archive_path" -C "$temp_dir" || fail "Could not extract $archive_name."
+source_binary="$temp_dir/$release_binary"
+[ -f "$source_binary" ] || fail "The release archive does not contain $release_binary."
+
+mkdir -p "$install_dir" || fail "Could not create $install_dir."
+installed_binary="$install_dir/$binary_name$binary_extension"
+install_temp="$install_dir/.$binary_name.tmp.$$"
+cp "$source_binary" "$install_temp" || fail "Could not write to $install_dir."
+chmod 0755 "$install_temp" || fail "Could not make the Explorer CLI executable."
+mv -f "$install_temp" "$installed_binary" || fail "Could not install the Explorer CLI to $installed_binary."
+install_temp=""
+step "Installed $binary_name to $installed_binary"
+
+printf '\n%sExplorer CLI is ready.%s\n' "$bold" "$reset"
+case ":${PATH:-}:" in
+ *":$install_dir:"*)
+ printf 'Run %sexplorer --help%s to get started.\n\n' "$bold" "$reset"
+ ;;
+ *)
+ printf 'Add the installation directory to your PATH:\n\n'
+ printf ' %sexport PATH="%s:$PATH"%s\n\n' "$dim" "$install_dir" "$reset"
+ ;;
+esac