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