feat(installer): add one-command installer flow and release smoke checks (#123)

This commit is contained in:
oxoxDev
2026-03-31 08:08:08 -07:00
committed by GitHub
parent e7a7f90fb6
commit c46adfa19a
6 changed files with 775 additions and 48 deletions
+35
View File
@@ -0,0 +1,35 @@
name: Installer Smoke
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
smoke-unix:
name: Smoke install.sh (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run installer dry-run
run: bash scripts/install.sh --dry-run --verbose
smoke-windows:
name: Smoke install.ps1 (windows-latest)
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run installer dry-run
shell: pwsh
run: ./scripts/install.ps1 -DryRun
+33 -4
View File
@@ -221,10 +221,10 @@ jobs:
args: --target x86_64-unknown-linux-gnu
target: x86_64-unknown-linux-gnu
artifact_suffix: ubuntu
# - platform: windows-latest
# args: --target x86_64-pc-windows-msvc
# target: x86_64-pc-windows-msvc
# artifact_suffix: windows
- platform: windows-latest
args: --target x86_64-pc-windows-msvc
target: x86_64-pc-windows-msvc
artifact_suffix: windows
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
@@ -691,6 +691,35 @@ jobs:
needs: [create-release, build-artifacts]
if: needs.build-artifacts.result == 'success'
steps:
- name: Validate required installer assets exist
uses: actions/github-script@v7
with:
script: |
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
const { owner, repo } = context.repo;
const { data: assets } = await github.rest.repos.listReleaseAssets({
owner,
repo,
release_id: releaseId,
per_page: 100,
});
const names = assets.map((a) => a.name);
const requiredPatterns = [
/OpenHuman_.*_aarch64\.dmg$/,
/OpenHuman_.*_x64\.dmg$/,
/OpenHuman_.*_amd64\.AppImage$/,
/OpenHuman_.*_amd64\.deb$/,
/(OpenHuman_.*_x64-setup\.exe$|OpenHuman_.*_x64.*\.msi$)/,
];
const missing = requiredPatterns.filter((pattern) => !names.some((name) => pattern.test(name)));
if (missing.length > 0) {
core.setFailed(`Missing required installer assets. Got: ${names.join(', ')}`);
return;
}
core.info('All required installer assets are present.');
- name: Publish release
uses: actions/github-script@v7
with:
+14 -2
View File
@@ -55,12 +55,24 @@ If you need an older version, browse [all releases](https://github.com/tinyhuman
If you want to build from source, see [`docs/BUILDING.md`](docs/BUILDING.md).
Or you can basically run this global install script
Install with one command:
```
COMING SOON curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
```
On Windows, use PowerShell:
`irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex`
What setup does:
- Resolves the latest stable release for your OS/arch
- Verifies release digest when available
- Installs locally without requiring system-wide admin rights by default
- macOS: installs `OpenHuman.app` in `~/Applications`
- Linux: installs `openhuman` AppImage in `~/.local/bin/openhuman` and creates a desktop entry
- Windows: installs from latest release MSI/EXE in per-user mode where supported
# Under the hood (Architecture)
OpenHuman is a **desktop monorepo**: **Rust** owns **business logic and execution**; the **UI** owns **interaction, layout, and OS integration**.
+29 -41
View File
@@ -42,55 +42,43 @@ yarn dev
## Install latest stable release (macOS/Linux)
Use this basic script to detect platform/arch and download the latest stable artifact from GitHub Releases:
Primary install command:
```bash
#!/usr/bin/env bash
set -euo pipefail
REPO="tinyhumansai/openhuman"
BASE="https://github.com/${REPO}/releases/latest/download"
OS="$(uname -s)"
ARCH="$(uname -m)"
if [[ "$OS" == "Darwin" ]]; then
if [[ "$ARCH" == "arm64" ]]; then
FILE="OpenHuman_0.49.32_aarch64.dmg"
else
FILE="OpenHuman_0.49.32_x64.dmg"
fi
elif [[ "$OS" == "Linux" ]]; then
# Prefer AppImage for broad compatibility.
FILE="OpenHuman_0.49.32_amd64.AppImage"
else
echo "Unsupported OS: $OS"
exit 1
fi
URL="${BASE}/${FILE}"
echo "Downloading: $URL"
curl -fL "$URL" -o "$FILE"
echo "Downloaded $FILE"
echo "Install it with your platform's normal installer flow."
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
```
Notes:
Installer behavior:
- The filename includes the release version and should be updated when a new stable release is cut.
- You can always manually download from:
- Website: https://tinyhuman.ai/openhuman
- Latest release: https://github.com/tinyhumansai/openhuman/releases/latest
- Resolves latest stable OpenHuman release for your platform
- Validates artifact digest when available
- Installs locally (no sudo by default)
- macOS: installs `OpenHuman.app` into `~/Applications`
- Linux: installs AppImage as `~/.local/bin/openhuman` and writes a desktop entry
Useful flags:
```bash
# Preview actions without writing files
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash -s -- --dry-run
```
## Windows (latest stable)
Download directly from the website or latest release page:
Use PowerShell:
- https://tinyhuman.ai/openhuman
- https://github.com/tinyhumansai/openhuman/releases/latest
```powershell
irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex
```
## Future improvements
Windows installer behavior:
- Replace hardcoded filenames with `latest.json` parsing
- Add checksum/signature verification
- Publish one-step global installers for all platforms
- Resolves latest stable release
- Downloads MSI/EXE for x64
- Verifies digest when available
- Runs per-user install where supported by installer package
Manual download links (all platforms):
- Website: https://tinyhuman.ai/openhuman
- Latest release: https://github.com/tinyhumansai/openhuman/releases/latest
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
OpenHuman installer for Windows.
.DESCRIPTION
Intended for:
irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex
#>
param(
[switch]$Help,
[switch]$Version,
[string]$Channel = "stable",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
$InstallerVersion = "1.0.0"
$Repo = "tinyhumansai/openhuman"
$LatestJsonUrl = "https://github.com/$Repo/releases/latest/download/latest.json"
$LatestReleaseApiUrl = "https://api.github.com/repos/$Repo/releases/latest"
function Write-Info([string]$Message) { Write-Host "-> $Message" -ForegroundColor Cyan }
function Write-Ok([string]$Message) { Write-Host "OK $Message" -ForegroundColor Green }
function Write-WarnMsg([string]$Message) { Write-Host "! $Message" -ForegroundColor Yellow }
function Write-Err([string]$Message) { Write-Host "x $Message" -ForegroundColor Red }
function Show-Usage {
@"
OpenHuman Installer (Windows)
Usage:
install.ps1 [-Channel stable] [-DryRun] [-Help] [-Version]
Examples:
irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex
.\scripts\install.ps1 -DryRun
"@
}
if ($Help) {
Show-Usage
exit 0
}
if ($Version) {
Write-Output "openhuman-installer $InstallerVersion"
exit 0
}
if ($Channel -ne "stable") {
Write-Err "Only -Channel stable is currently supported."
exit 1
}
if ($env:OS -ne "Windows_NT") {
Write-Err "This installer is for Windows only."
exit 1
}
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
if ($arch -notin @("x64", "amd64")) {
Write-Err "Unsupported architecture: $arch (Windows x64 required)."
exit 1
}
Write-Ok "Detected platform: windows/x64"
$release = $null
$releaseTag = ""
$assetName = ""
$assetUrl = ""
$assetDigest = ""
function Select-WindowsAssetFromRelease([object]$Rel) {
$assets = @($Rel.assets)
if (-not $assets -or $assets.Count -eq 0) {
return $null
}
$msi = $assets | Where-Object { $_.name -match 'OpenHuman_.*x64.*\.msi$' } | Select-Object -First 1
if ($msi) { return $msi }
$exe = $assets | Where-Object { $_.name -match 'OpenHuman_.*x64.*\.exe$' } | Select-Object -First 1
if ($exe) { return $exe }
return $null
}
try {
$release = Invoke-RestMethod -Uri $LatestReleaseApiUrl
$releaseTag = ($release.tag_name -replace '^v', '')
$selected = Select-WindowsAssetFromRelease -Rel $release
if ($selected) {
$assetName = $selected.name
$assetUrl = $selected.browser_download_url
if ($selected.digest) {
$assetDigest = ($selected.digest -replace '^sha256:', '')
}
}
} catch {
Write-WarnMsg "Could not query release API: $($_.Exception.Message)"
}
if (-not $assetUrl) {
Write-Err "No Windows x64 installer artifact found in latest release."
Write-Err "Ensure release workflow publishes Windows MSI/EXE assets."
exit 1
}
Write-Ok "Resolved latest release ($releaseTag): $assetName"
$tmpFile = Join-Path $env:TEMP $assetName
if ($DryRun) {
Write-Output "DRY RUN: download $assetUrl -> $tmpFile"
} else {
Write-Info "Downloading $assetName"
Invoke-WebRequest -Uri $assetUrl -OutFile $tmpFile
}
if ($assetDigest) {
if ($DryRun) {
Write-Output "DRY RUN: verify SHA256 $assetDigest"
} else {
$fileHash = (Get-FileHash -Path $tmpFile -Algorithm SHA256).Hash.ToLowerInvariant()
if ($fileHash -ne $assetDigest.ToLowerInvariant()) {
Write-Err "SHA256 mismatch for $assetName"
Write-Err "Expected: $assetDigest"
Write-Err "Actual: $fileHash"
exit 1
}
Write-Ok "Integrity verified (sha256)"
}
} else {
Write-WarnMsg "No SHA256 digest available for $assetName; skipping integrity verification."
}
if ($DryRun) {
if ($assetName -like "*.msi") {
Write-Output "DRY RUN: msiexec /i `"$tmpFile`" MSIINSTALLPERUSER=1 ALLUSERS=2 /qn /norestart"
} else {
Write-Output "DRY RUN: Start-Process `"$tmpFile`" -Wait"
}
exit 0
}
Write-Info "Installing OpenHuman"
if ($assetName -like "*.msi") {
$args = "/i `"$tmpFile`" MSIINSTALLPERUSER=1 ALLUSERS=2 /qn /norestart"
$proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Err "MSI install failed with exit code $($proc.ExitCode)."
exit 1
}
} elseif ($assetName -like "*.exe") {
$proc = Start-Process -FilePath $tmpFile -Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Err "Installer exited with code $($proc.ExitCode)."
exit 1
}
} else {
Write-Err "Unsupported Windows installer type: $assetName"
exit 1
}
$expectedPaths = @(
"$env:LOCALAPPDATA\Programs\OpenHuman\OpenHuman.exe",
"$env:ProgramFiles\OpenHuman\OpenHuman.exe"
)
$launchPath = $expectedPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
Write-Output ""
Write-Output "OpenHuman is ready."
if ($launchPath) {
Write-Output "Launch: `"$launchPath`""
Write-Output "Uninstall: Settings -> Apps -> Installed apps -> OpenHuman"
} else {
Write-WarnMsg "Could not locate installed executable automatically."
Write-Output "Try launching OpenHuman from Start Menu."
Write-Output "Uninstall: Settings -> Apps -> Installed apps -> OpenHuman"
}
+481 -1
View File
@@ -1 +1,481 @@
echo "Coming soon. For now, you can download the latest desktop build from the website at [tinyhuman.ai/openhuman](https://tinyhuman.ai/openhuman)."
#!/usr/bin/env bash
# OpenHuman Installer (macOS/Linux)
# Usage:
# curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
set -euo pipefail
INSTALLER_VERSION="1.0.0"
REPO="tinyhumansai/openhuman"
LATEST_JSON_URL="https://github.com/${REPO}/releases/latest/download/latest.json"
LATEST_RELEASE_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
CHANNEL="stable"
DRY_RUN=false
VERBOSE=false
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
CYAN=''
NC=''
fi
log_info() { echo -e "${CYAN}${NC} $*"; }
log_ok() { echo -e "${GREEN}${NC} $*"; }
log_warn() { echo -e "${YELLOW}!${NC} $*"; }
log_err() { echo -e "${RED}x${NC} $*" >&2; }
usage() {
cat <<'EOF'
OpenHuman Installer
Usage: install.sh [OPTIONS]
Options:
--help Show help
--version Show installer version
--channel VALUE Release channel (default: stable)
--dry-run Print actions without mutating local files
--verbose Enable verbose output
Examples:
curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash
curl -fsSL ... | bash -s -- --dry-run
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
usage
exit 0
;;
--version)
echo "openhuman-installer ${INSTALLER_VERSION}"
exit 0
;;
--channel)
CHANNEL="${2:-}"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
*)
log_err "Unknown option: $1"
usage
exit 1
;;
esac
done
if [ "${CHANNEL}" != "stable" ]; then
log_err "Only --channel stable is currently supported."
exit 1
fi
for cmd in curl mktemp tar; do
if ! command -v "${cmd}" >/dev/null 2>&1; then
log_err "Missing required command: ${cmd}"
exit 1
fi
done
OS_RAW="$(uname -s)"
ARCH_RAW="$(uname -m)"
OS=""
ARCH=""
PLATFORM_KEY=""
case "${OS_RAW}" in
Darwin) OS="darwin" ;;
Linux) OS="linux" ;;
CYGWIN*|MINGW*|MSYS*)
log_err "Windows detected. Use PowerShell installer:"
echo " irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex"
exit 1
;;
*)
log_err "Unsupported OS: ${OS_RAW}"
exit 1
;;
esac
case "${ARCH_RAW}" in
x86_64|amd64) ARCH="x86_64" ;;
arm64|aarch64) ARCH="aarch64" ;;
*)
log_err "Unsupported architecture: ${ARCH_RAW}"
exit 1
;;
esac
if [ "${OS}" = "linux" ] && [ "${ARCH}" != "x86_64" ]; then
log_err "Linux installer currently supports x86_64 only."
exit 1
fi
if [ "${OS}" = "darwin" ] && [ "${ARCH}" = "aarch64" ]; then
PLATFORM_KEY="darwin-aarch64"
elif [ "${OS}" = "darwin" ] && [ "${ARCH}" = "x86_64" ]; then
PLATFORM_KEY="darwin-x86_64"
elif [ "${OS}" = "linux" ] && [ "${ARCH}" = "x86_64" ]; then
PLATFORM_KEY="linux-x86_64"
fi
log_ok "Detected platform: ${OS}/${ARCH}"
TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "${TMP_DIR}"
}
trap cleanup EXIT
LATEST_JSON_PATH="${TMP_DIR}/latest.json"
RELEASE_JSON_PATH="${TMP_DIR}/release.json"
LATEST_VERSION=""
ASSET_URL=""
ASSET_NAME=""
ASSET_SHA256=""
resolve_from_latest_json() {
if ! curl -fsSL "${LATEST_JSON_URL}" -o "${LATEST_JSON_PATH}"; then
return 1
fi
if ! command -v python3 >/dev/null 2>&1; then
log_warn "python3 is not available; cannot parse latest.json reliably."
return 1
fi
local parsed
parsed="$(python3 - "${LATEST_JSON_PATH}" "${PLATFORM_KEY}" <<'PY'
import json, sys
path, key = sys.argv[1], sys.argv[2]
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
version = data.get("version", "")
platforms = data.get("platforms", {})
entry = platforms.get(key) or platforms.get(f"{key}-appimage") or platforms.get(f"{key}-app")
url = ""
if isinstance(entry, dict):
url = entry.get("url", "")
print(version)
print(url)
PY
)" || return 1
LATEST_VERSION="$(echo "${parsed}" | sed -n '1p')"
ASSET_URL="$(echo "${parsed}" | sed -n '2p')"
ASSET_NAME="$(basename "${ASSET_URL}")"
[ -n "${ASSET_URL}" ]
}
resolve_from_release_api() {
if ! curl -fsSL "${LATEST_RELEASE_API_URL}" -o "${RELEASE_JSON_PATH}"; then
return 1
fi
if ! command -v python3 >/dev/null 2>&1; then
log_warn "python3 is not available; cannot parse release API fallback."
return 1
fi
local parsed
parsed="$(python3 - "${RELEASE_JSON_PATH}" "${OS}" "${ARCH}" <<'PY'
import json, re, sys
path, os_name, arch = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
tag = (data.get("tag_name") or "").lstrip("v")
assets = data.get("assets", [])
def choose_asset():
names = [a.get("name", "") for a in assets]
chosen = None
if os_name == "darwin" and arch == "aarch64":
for n in names:
if re.search(r"aarch64.*\\.app\\.tar\\.gz$", n):
chosen = n
break
if not chosen:
for n in names:
if re.search(r"aarch64\\.dmg$", n):
chosen = n
break
elif os_name == "darwin" and arch == "x86_64":
for n in names:
if re.search(r"(x86_64-apple-darwin|x64).*\\.app\\.tar\\.gz$", n):
chosen = n
break
if not chosen:
for n in names:
if re.search(r"x64\\.dmg$", n):
chosen = n
break
elif os_name == "linux" and arch == "x86_64":
for n in names:
if n.endswith(".AppImage"):
chosen = n
break
if not chosen:
return "", "", ""
for asset in assets:
if asset.get("name") == chosen:
return chosen, asset.get("browser_download_url", ""), (asset.get("digest", "") or "").replace("sha256:", "")
return "", "", ""
name, url, digest = choose_asset()
print(tag)
print(name)
print(url)
print(digest)
PY
)" || return 1
if [ -z "${LATEST_VERSION}" ]; then
LATEST_VERSION="$(echo "${parsed}" | sed -n '1p')"
fi
ASSET_NAME="$(echo "${parsed}" | sed -n '2p')"
ASSET_URL="$(echo "${parsed}" | sed -n '3p')"
ASSET_SHA256="$(echo "${parsed}" | sed -n '4p')"
[ -n "${ASSET_URL}" ]
}
resolve_release_digest() {
if [ -z "${ASSET_NAME}" ]; then
return 0
fi
if [ ! -s "${RELEASE_JSON_PATH}" ]; then
if ! curl -fsSL "${LATEST_RELEASE_API_URL}" -o "${RELEASE_JSON_PATH}"; then
return 0
fi
fi
if ! command -v python3 >/dev/null 2>&1; then
return 0
fi
local digest
digest="$(python3 - "${RELEASE_JSON_PATH}" "${ASSET_NAME}" <<'PY'
import json, sys
path, name = sys.argv[1], sys.argv[2]
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
for asset in data.get("assets", []):
if asset.get("name") == name:
d = asset.get("digest", "") or ""
print(d.replace("sha256:", ""))
break
PY
)"
if [ -n "${digest}" ]; then
ASSET_SHA256="${digest}"
fi
}
if resolve_from_latest_json; then
log_ok "Resolved latest release via latest.json (${LATEST_VERSION})"
else
log_warn "latest.json lookup failed. Falling back to releases API."
if ! resolve_from_release_api; then
log_err "Could not resolve a compatible asset for ${OS}/${ARCH}."
exit 1
fi
log_ok "Resolved latest release via releases API (${LATEST_VERSION})"
fi
resolve_release_digest
if [ -z "${ASSET_URL}" ]; then
log_err "Could not determine download URL for ${OS}/${ARCH}."
exit 1
fi
DOWNLOAD_PATH="${TMP_DIR}/${ASSET_NAME}"
log_info "Downloading ${ASSET_NAME}"
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: curl -fL ${ASSET_URL} -o ${DOWNLOAD_PATH}"
else
curl -fL "${ASSET_URL}" -o "${DOWNLOAD_PATH}"
fi
compute_sha256() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${file}" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "${file}" | awk '{print $1}'
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "${file}" | awk '{print $2}'
else
return 1
fi
}
if [ -n "${ASSET_SHA256}" ]; then
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: verify sha256 ${ASSET_SHA256} for ${DOWNLOAD_PATH}"
else
actual_sha256="$(compute_sha256 "${DOWNLOAD_PATH}" || true)"
if [ -z "${actual_sha256}" ]; then
log_warn "No checksum command available; skipping digest verification."
elif [ "${actual_sha256}" != "${ASSET_SHA256}" ]; then
log_err "SHA256 mismatch for ${ASSET_NAME}"
log_err "Expected: ${ASSET_SHA256}"
log_err "Actual: ${actual_sha256}"
exit 1
else
log_ok "Integrity verified (sha256)"
fi
fi
else
log_warn "No SHA256 digest available for ${ASSET_NAME}; skipping integrity verification."
fi
ensure_local_bin_path() {
local bin_dir="${HOME}/.local/bin"
if echo ":${PATH}:" | grep -q ":${bin_dir}:"; then
return 0
fi
local shell_name config_file
shell_name="$(basename "${SHELL:-/bin/bash}")"
case "${shell_name}" in
zsh) config_file="${HOME}/.zshrc" ;;
bash) config_file="${HOME}/.bashrc" ;;
*) config_file="${HOME}/.profile" ;;
esac
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: ensure ${bin_dir} in PATH via ${config_file}"
return 0
fi
if [ ! -f "${config_file}" ]; then
touch "${config_file}"
fi
if ! grep -q '.local/bin' "${config_file}"; then
{
echo ""
echo '# OpenHuman installer - ensure local user binaries are on PATH'
echo 'export PATH="$HOME/.local/bin:$PATH"'
} >> "${config_file}"
log_ok "Added ~/.local/bin to ${config_file}"
fi
}
install_macos() {
local apps_dir="${HOME}/Applications"
local app_path="${apps_dir}/OpenHuman.app"
mkdir -p "${apps_dir}"
if [[ "${ASSET_NAME}" == *.app.tar.gz ]]; then
log_info "Installing OpenHuman.app into ${apps_dir}"
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: tar -xzf ${DOWNLOAD_PATH} -C ${TMP_DIR}"
echo "DRY RUN: replace ${app_path}"
else
tar -xzf "${DOWNLOAD_PATH}" -C "${TMP_DIR}"
if [ ! -d "${TMP_DIR}/OpenHuman.app" ]; then
log_err "Archive did not contain OpenHuman.app"
exit 1
fi
rm -rf "${app_path}"
cp -R "${TMP_DIR}/OpenHuman.app" "${app_path}"
fi
elif [[ "${ASSET_NAME}" == *.dmg ]]; then
log_info "Mounting DMG and copying OpenHuman.app"
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: hdiutil attach ${DOWNLOAD_PATH}"
echo "DRY RUN: copy OpenHuman.app to ${app_path}"
else
if ! command -v hdiutil >/dev/null 2>&1; then
log_err "hdiutil not available, cannot install from DMG."
exit 1
fi
mount_output="$(hdiutil attach "${DOWNLOAD_PATH}" -nobrowse)"
mount_point="$(echo "${mount_output}" | awk '/\/Volumes\// {print $NF; exit}')"
if [ -z "${mount_point}" ] || [ ! -d "${mount_point}/OpenHuman.app" ]; then
log_err "Could not find OpenHuman.app in mounted DMG."
echo "${mount_output}"
exit 1
fi
rm -rf "${app_path}"
cp -R "${mount_point}/OpenHuman.app" "${app_path}"
hdiutil detach "${mount_point}" >/dev/null
fi
else
log_err "Unsupported macOS asset type: ${ASSET_NAME}"
exit 1
fi
log_ok "Installed at ${app_path}"
echo ""
echo "OpenHuman is ready."
echo "Launch: open \"${app_path}\""
echo "Uninstall: rm -rf \"${app_path}\""
}
install_linux() {
local bin_dir="${HOME}/.local/bin"
local app_path="${bin_dir}/openhuman"
local desktop_dir="${HOME}/.local/share/applications"
local desktop_file="${desktop_dir}/openhuman.desktop"
mkdir -p "${bin_dir}" "${desktop_dir}"
if [[ "${ASSET_NAME}" != *.AppImage ]]; then
log_err "Expected AppImage for Linux install, got: ${ASSET_NAME}"
exit 1
fi
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: install ${DOWNLOAD_PATH} -> ${app_path}"
else
cp "${DOWNLOAD_PATH}" "${app_path}"
chmod +x "${app_path}"
fi
ensure_local_bin_path
if [ "${DRY_RUN}" = true ]; then
echo "DRY RUN: write ${desktop_file}"
else
cat > "${desktop_file}" <<EOF
[Desktop Entry]
Type=Application
Name=OpenHuman
Comment=OpenHuman desktop assistant
Exec=${app_path}
TryExec=${app_path}
Terminal=false
Categories=Utility;
EOF
fi
log_ok "Installed binary at ${app_path}"
echo ""
echo "OpenHuman is ready."
echo "Launch: ${app_path}"
echo "Uninstall: rm -f \"${app_path}\" \"${desktop_file}\""
}
if [ "${OS}" = "darwin" ]; then
install_macos
else
install_linux
fi