From 5ec0667fb3c7be05f692ffbc0b10222f0700d064 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 5 May 2026 13:04:33 -0500 Subject: [PATCH] ci(desktop): automate desktop releases Add GitHub Actions release channels for signed desktop installers and document the stable/nightly download paths. --- .github/workflows/desktop-release.yml | 341 ++++++++++++++++++ apps/desktop/README.md | 29 ++ .../electron/entitlements.mac.inherit.plist | 12 + apps/desktop/electron/entitlements.mac.plist | 14 + apps/desktop/package.json | 7 +- apps/desktop/scripts/notarize-artifact.cjs | 74 ++++ apps/desktop/scripts/notarize.cjs | 100 +++++ website/docs/getting-started/installation.md | 13 +- website/docs/getting-started/quickstart.md | 5 + website/docs/index.md | 3 + website/docusaurus.config.ts | 7 + 11 files changed, 603 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/desktop-release.yml create mode 100644 apps/desktop/electron/entitlements.mac.inherit.plist create mode 100644 apps/desktop/electron/entitlements.mac.plist create mode 100644 apps/desktop/scripts/notarize-artifact.cjs create mode 100644 apps/desktop/scripts/notarize.cjs diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 00000000000..c99c6f1b947 --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,341 @@ +name: Desktop Release + +on: + push: + branches: [main] + release: + types: [published] + workflow_dispatch: + inputs: + channel: + description: Release channel to build + required: true + default: nightly + type: choice + options: + - nightly + - stable + release_tag: + description: "Required when channel=stable (example: v2026.5.5)" + required: false + type: string + +permissions: + contents: write + +concurrency: + group: desktop-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + prepare: + if: github.repository == 'NousResearch/hermes-agent' + runs-on: ubuntu-latest + outputs: + channel: ${{ steps.meta.outputs.channel }} + release_name: ${{ steps.meta.outputs.release_name }} + release_tag: ${{ steps.meta.outputs.release_tag }} + version: ${{ steps.meta.outputs.version }} + is_stable: ${{ steps.meta.outputs.is_stable }} + steps: + - id: meta + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_CHANNEL: ${{ github.event.inputs.channel }} + INPUT_RELEASE_TAG: ${{ github.event.inputs.release_tag }} + RELEASE_TAG_FROM_EVENT: ${{ github.event.release.tag_name }} + GITHUB_SHA: ${{ github.sha }} + run: | + set -euo pipefail + + channel="nightly" + release_tag="desktop-nightly" + is_stable="false" + + if [[ "$EVENT_NAME" == "release" ]]; then + channel="stable" + release_tag="$RELEASE_TAG_FROM_EVENT" + is_stable="true" + elif [[ "$EVENT_NAME" == "workflow_dispatch" && "$INPUT_CHANNEL" == "stable" ]]; then + channel="stable" + release_tag="$INPUT_RELEASE_TAG" + is_stable="true" + fi + + if [[ "$channel" == "stable" ]]; then + if [[ -z "$release_tag" ]]; then + echo "Stable desktop releases require a release tag." >&2 + exit 1 + fi + + version="${release_tag#v}" + release_name="Hermes Desktop ${release_tag}" + else + stamp="$(date -u +%Y%m%d)" + short_sha="${GITHUB_SHA::7}" + version="0.0.0-nightly.${stamp}.${short_sha}" + release_name="Hermes Desktop Nightly ${stamp}-${short_sha}" + fi + + { + echo "channel=$channel" + echo "release_name=$release_name" + echo "release_tag=$release_tag" + echo "version=$version" + echo "is_stable=$is_stable" + } >> "$GITHUB_OUTPUT" + + build: + if: github.repository == 'NousResearch/hermes-agent' + needs: prepare + strategy: + fail-fast: false + matrix: + include: + - platform: mac + runner: macos-latest + build_args: --mac dmg zip + - platform: win + runner: windows-latest + build_args: --win nsis msi + runs-on: ${{ matrix.runner }} + env: + DESKTOP_CHANNEL: ${{ needs.prepare.outputs.channel }} + DESKTOP_VERSION: ${{ needs.prepare.outputs.version }} + MAC_CSC_LINK: ${{ secrets.CSC_LINK }} + MAC_CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }} + WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.11" + + - name: Enforce signing gates for stable releases + if: needs.prepare.outputs.is_stable == 'true' + shell: bash + run: | + set -euo pipefail + missing=() + + if [[ "${{ matrix.platform }}" == "mac" ]]; then + [[ -z "${MAC_CSC_LINK:-}" ]] && missing+=("CSC_LINK") + [[ -z "${MAC_CSC_KEY_PASSWORD:-}" ]] && missing+=("CSC_KEY_PASSWORD") + [[ -z "${APPLE_API_KEY:-}" ]] && missing+=("APPLE_API_KEY") + [[ -z "${APPLE_API_KEY_ID:-}" ]] && missing+=("APPLE_API_KEY_ID") + [[ -z "${APPLE_API_ISSUER:-}" ]] && missing+=("APPLE_API_ISSUER") + else + [[ -z "${WIN_CSC_LINK:-}" ]] && missing+=("WIN_CSC_LINK") + [[ -z "${WIN_CSC_KEY_PASSWORD:-}" ]] && missing+=("WIN_CSC_KEY_PASSWORD") + fi + + if (( ${#missing[@]} > 0 )); then + echo "::error::Stable desktop release missing required secrets: ${missing[*]}" + exit 1 + fi + + - name: Install workspace dependencies + run: npm ci + + - name: Build bundled TUI payload + run: npm --prefix ui-tui run build + + - name: Build desktop renderer + run: npm --prefix apps/desktop run build + + - name: Stage Hermes payload + run: npm --prefix apps/desktop run stage:hermes + + - name: Map macOS signing credentials + if: matrix.platform == 'mac' + shell: bash + run: | + set -euo pipefail + has_link=0 + has_pass=0 + [[ -n "${MAC_CSC_LINK:-}" ]] && has_link=1 + [[ -n "${MAC_CSC_KEY_PASSWORD:-}" ]] && has_pass=1 + + if [[ $has_link -eq 1 && $has_pass -eq 1 ]]; then + echo "CSC_LINK=${MAC_CSC_LINK}" >> "$GITHUB_ENV" + echo "CSC_KEY_PASSWORD=${MAC_CSC_KEY_PASSWORD}" >> "$GITHUB_ENV" + elif [[ $has_link -eq 1 || $has_pass -eq 1 ]]; then + echo "::error::macOS signing secrets are partially configured. Set both CSC_LINK and CSC_KEY_PASSWORD." + exit 1 + fi + + - name: Map Windows signing credentials + if: matrix.platform == 'win' + shell: bash + run: | + set -euo pipefail + has_link=0 + has_pass=0 + [[ -n "${WIN_CSC_LINK:-}" ]] && has_link=1 + [[ -n "${WIN_CSC_KEY_PASSWORD:-}" ]] && has_pass=1 + + if [[ $has_link -eq 1 && $has_pass -eq 1 ]]; then + echo "CSC_LINK=${WIN_CSC_LINK}" >> "$GITHUB_ENV" + echo "CSC_KEY_PASSWORD=${WIN_CSC_KEY_PASSWORD}" >> "$GITHUB_ENV" + echo "CSC_FOR_PULL_REQUEST=true" >> "$GITHUB_ENV" + elif [[ $has_link -eq 1 || $has_pass -eq 1 ]]; then + echo "::error::Windows signing secrets are partially configured. Set both WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD." + exit 1 + fi + + - name: Build desktop installers + shell: bash + run: | + set -euo pipefail + npm --prefix apps/desktop exec electron-builder -- \ + ${{ matrix.build_args }} \ + --publish never \ + --config.extraMetadata.version="${DESKTOP_VERSION}" \ + --config.extraMetadata.desktopChannel="${DESKTOP_CHANNEL}" \ + '--config.artifactName=Hermes-${version}-${env.DESKTOP_CHANNEL}-${os}-${arch}.${ext}' + + - name: Notarize and staple macOS DMG + if: matrix.platform == 'mac' && needs.prepare.outputs.is_stable == 'true' + shell: bash + run: | + set -euo pipefail + dmg_path="$(ls apps/desktop/release/*.dmg | head -n 1)" + node apps/desktop/scripts/notarize-artifact.cjs "$dmg_path" + + - name: Validate macOS notarization and Gatekeeper trust + if: matrix.platform == 'mac' && needs.prepare.outputs.is_stable == 'true' + shell: bash + run: | + set -euo pipefail + app_path="$(ls -d apps/desktop/release/mac*/Hermes.app | head -n 1)" + dmg_path="$(ls apps/desktop/release/*.dmg | head -n 1)" + xcrun stapler validate "$app_path" + xcrun stapler validate "$dmg_path" + spctl --assess --type execute --verbose=4 "$app_path" + + - name: Generate desktop checksums + shell: bash + run: | + set -euo pipefail + node <<'EOF' + const crypto = require('node:crypto') + const fs = require('node:fs') + const path = require('node:path') + + const releaseDir = path.resolve('apps/desktop/release') + const platform = process.env.PLATFORM + const extensions = platform === 'mac' ? ['.dmg', '.zip'] : ['.exe', '.msi'] + const files = fs + .readdirSync(releaseDir) + .filter(name => extensions.some(ext => name.endsWith(ext))) + .sort() + + if (!files.length) { + throw new Error(`No release artifacts were produced for ${platform}`) + } + + const lines = files.map(name => { + const full = path.join(releaseDir, name) + const hash = crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex') + return `${hash} ${name}` + }) + fs.writeFileSync(path.join(releaseDir, `SHA256SUMS-${platform}.txt`), `${lines.join('\n')}\n`) + EOF + env: + PLATFORM: ${{ matrix.platform }} + + - name: Upload packaged desktop artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: desktop-${{ matrix.platform }} + path: | + apps/desktop/release/*.dmg + apps/desktop/release/*.zip + apps/desktop/release/*.exe + apps/desktop/release/*.msi + apps/desktop/release/SHA256SUMS-${{ matrix.platform }}.txt + if-no-files-found: error + + publish: + if: github.repository == 'NousResearch/hermes-agent' + needs: [prepare, build] + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + CHANNEL: ${{ needs.prepare.outputs.channel }} + RELEASE_NAME: ${{ needs.prepare.outputs.release_name }} + RELEASE_TAG: ${{ needs.prepare.outputs.release_tag }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: desktop-* + merge-multiple: true + path: dist/desktop + + - name: Publish desktop assets to GitHub release + shell: bash + run: | + set -euo pipefail + shopt -s globstar nullglob + + files=( + dist/desktop/**/*.dmg + dist/desktop/**/*.zip + dist/desktop/**/*.exe + dist/desktop/**/*.msi + dist/desktop/**/SHA256SUMS-*.txt + ) + + if (( ${#files[@]} == 0 )); then + echo "No desktop artifacts were downloaded for publishing." >&2 + exit 1 + fi + + if [[ "$CHANNEL" == "nightly" ]]; then + git tag -f "$RELEASE_TAG" "$GITHUB_SHA" + git push origin "refs/tags/$RELEASE_TAG" --force + + notes="Automated nightly desktop build from main. This prerelease is replaced on each new run." + + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + while IFS= read -r asset_name; do + gh release delete-asset "$RELEASE_TAG" "$asset_name" --yes + done < <(gh release view "$RELEASE_TAG" --json assets -q '.assets[].name') + + gh release edit "$RELEASE_TAG" \ + --title "$RELEASE_NAME" \ + --prerelease \ + --notes "$notes" + else + gh release create "$RELEASE_TAG" \ + --target "$GITHUB_SHA" \ + --title "$RELEASE_NAME" \ + --notes "$notes" \ + --prerelease + fi + else + if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + notes="Automated desktop artifacts attached by desktop-release workflow." + gh release create "$RELEASE_TAG" \ + --target "$GITHUB_SHA" \ + --title "$RELEASE_NAME" \ + --notes "$notes" + fi + fi + + gh release upload "$RELEASE_TAG" "${files[@]}" --clobber diff --git a/apps/desktop/README.md b/apps/desktop/README.md index ba33233416b..644edd30882 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -67,6 +67,35 @@ npm run dist:win # NSIS + MSI Before packaging, `stage:hermes` copies the Python Hermes payload into `build/hermes-agent`. Electron Builder then ships it as `Contents/Resources/hermes-agent`. +## Automated Releases + +Desktop installers are published by [`.github/workflows/desktop-release.yml`](../../.github/workflows/desktop-release.yml) with two channels: + +- **Stable:** runs on published GitHub releases and uploads signed artifacts to that release tag. +- **Nightly:** runs on `main` pushes and updates the rolling `desktop-nightly` prerelease. + +The workflow injects a channel-aware desktop version at build time: + +- stable: derived from the release tag (for example `v2026.5.5` -> `2026.5.5`) +- nightly: `0.0.0-nightly.YYYYMMDD.` + +Artifact names include channel, platform, and architecture: + +```text +Hermes----. +``` + +Each run also publishes `SHA256SUMS-.txt` so installers can be verified. + +### Stable release gates + +Stable builds fail fast if signing credentials are missing: + +- macOS signing + notarization: `CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_API_KEY`, `APPLE_API_KEY_ID`, `APPLE_API_ISSUER` +- Windows signing: `WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD` + +Stable macOS builds also validate stapling and Gatekeeper assessment in CI before upload. + ## Icons Desktop icons live in `assets/`: diff --git a/apps/desktop/electron/entitlements.mac.inherit.plist b/apps/desktop/electron/entitlements.mac.inherit.plist new file mode 100644 index 00000000000..f2eb2ecb0ad --- /dev/null +++ b/apps/desktop/electron/entitlements.mac.inherit.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/apps/desktop/electron/entitlements.mac.plist b/apps/desktop/electron/entitlements.mac.plist new file mode 100644 index 00000000000..53fdf0fc437 --- /dev/null +++ b/apps/desktop/electron/entitlements.mac.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + + diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 59ffc3ff87f..401bf570074 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -100,7 +100,7 @@ "appId": "com.nousresearch.hermes", "productName": "Hermes", "executableName": "Hermes", - "artifactName": "Hermes-${version}-${arch}.${ext}", + "artifactName": "Hermes-${version}-${os}-${arch}.${ext}", "icon": "assets/icon", "directories": { "output": "release" @@ -119,11 +119,14 @@ } ], "asar": true, + "afterSign": "scripts/notarize.cjs", "asarUnpack": [ "dist/**" ], "mac": { "category": "public.app-category.developer-tools", + "entitlements": "electron/entitlements.mac.plist", + "entitlementsInherit": "electron/entitlements.mac.inherit.plist", "extendInfo": { "CFBundleDisplayName": "Hermes", "CFBundleExecutable": "Hermes", @@ -131,6 +134,8 @@ "NSAudioCaptureUsageDescription": "Hermes uses audio capture for voice conversations.", "NSMicrophoneUsageDescription": "Hermes uses the microphone for voice input and voice conversations." }, + "gatekeeperAssess": false, + "hardenedRuntime": true, "target": [ "dmg", "zip" diff --git a/apps/desktop/scripts/notarize-artifact.cjs b/apps/desktop/scripts/notarize-artifact.cjs new file mode 100644 index 00000000000..32fe8fff008 --- /dev/null +++ b/apps/desktop/scripts/notarize-artifact.cjs @@ -0,0 +1,74 @@ +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const { execFile } = require('node:child_process') + +function run(command, args) { + return new Promise((resolve, reject) => { + execFile(command, args, (error, stdout, stderr) => { + if (error) { + reject(new Error(`${command} ${args.join(' ')} failed: ${stderr?.trim() || stdout?.trim() || error.message}`)) + return + } + resolve() + }) + }) +} + +function inlineKeyLooksValid(value) { + return value.includes('BEGIN PRIVATE KEY') && value.includes('END PRIVATE KEY') +} + +function resolveApiKeyPath(rawValue) { + const value = String(rawValue || '').trim() + if (!value) return { keyPath: '', cleanup: () => {} } + + if (fs.existsSync(value)) { + return { keyPath: value, cleanup: () => {} } + } + + if (!inlineKeyLooksValid(value)) { + throw new Error('APPLE_API_KEY must be a file path or inline .p8 key content') + } + + const tempPath = path.join(os.tmpdir(), `hermes-notary-${Date.now()}-${process.pid}.p8`) + fs.writeFileSync(tempPath, value, 'utf8') + return { + keyPath: tempPath, + cleanup: () => fs.rmSync(tempPath, { force: true }) + } +} + +async function main() { + const artifactPath = process.argv[2] + if (!artifactPath || !fs.existsSync(artifactPath)) { + throw new Error(`Missing artifact to notarize: ${artifactPath || '(none)'}`) + } + + const profile = String(process.env.APPLE_NOTARY_PROFILE || '').trim() + if (profile) { + await run('xcrun', ['notarytool', 'submit', artifactPath, '--keychain-profile', profile, '--wait']) + await run('xcrun', ['stapler', 'staple', '-v', artifactPath]) + return + } + + const keyId = String(process.env.APPLE_API_KEY_ID || '').trim() + const issuer = String(process.env.APPLE_API_ISSUER || '').trim() + const rawApiKey = process.env.APPLE_API_KEY + if (!rawApiKey || !keyId || !issuer) { + throw new Error('APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER are required') + } + + const { keyPath, cleanup } = resolveApiKeyPath(rawApiKey) + try { + await run('xcrun', ['notarytool', 'submit', artifactPath, '--key', keyPath, '--key-id', keyId, '--issuer', issuer, '--wait']) + await run('xcrun', ['stapler', 'staple', '-v', artifactPath]) + } finally { + cleanup() + } +} + +main().catch(error => { + console.error(error.message) + process.exit(1) +}) diff --git a/apps/desktop/scripts/notarize.cjs b/apps/desktop/scripts/notarize.cjs new file mode 100644 index 00000000000..1508e18e803 --- /dev/null +++ b/apps/desktop/scripts/notarize.cjs @@ -0,0 +1,100 @@ +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const { execFile } = require('node:child_process') + +function run(command, args) { + return new Promise((resolve, reject) => { + execFile(command, args, (error, stdout, stderr) => { + if (error) { + reject( + new Error( + `${command} ${args.join(' ')} failed: ${stderr?.trim() || stdout?.trim() || error.message}` + ) + ) + return + } + resolve({ stdout, stderr }) + }) + }) +} + +function inlineKeyLooksValid(value) { + return value.includes('BEGIN PRIVATE KEY') && value.includes('END PRIVATE KEY') +} + +function resolveApiKeyPath(rawValue) { + const value = String(rawValue || '').trim() + if (!value) return { keyPath: '', cleanup: () => {} } + + if (fs.existsSync(value)) { + return { keyPath: value, cleanup: () => {} } + } + + if (!inlineKeyLooksValid(value)) { + throw new Error('APPLE_API_KEY must be a file path or inline .p8 key content') + } + + const tempPath = path.join(os.tmpdir(), `hermes-notary-${Date.now()}-${process.pid}.p8`) + fs.writeFileSync(tempPath, value, 'utf8') + return { + keyPath: tempPath, + cleanup: () => { + try { + fs.rmSync(tempPath, { force: true }) + } catch { + // Best-effort cleanup. + } + } + } +} + +exports.default = async function notarize(context) { + const { electronPlatformName, appOutDir, packager } = context + if (electronPlatformName !== 'darwin') return + + const appName = packager.appInfo.productFilename + const appPath = path.join(appOutDir, `${appName}.app`) + if (!fs.existsSync(appPath)) { + throw new Error(`Cannot notarize missing app bundle: ${appPath}`) + } + + const profile = String(process.env.APPLE_NOTARY_PROFILE || '').trim() + if (profile) { + const zipPath = path.join(appOutDir, `${appName}.zip`) + await run('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appPath, zipPath]) + await run('xcrun', ['notarytool', 'submit', zipPath, '--keychain-profile', profile, '--wait']) + await run('xcrun', ['stapler', 'staple', '-v', appPath]) + try { + fs.rmSync(zipPath, { force: true }) + } catch { + // Best-effort cleanup. + } + return + } + + const keyId = String(process.env.APPLE_API_KEY_ID || '').trim() + const issuer = String(process.env.APPLE_API_ISSUER || '').trim() + const rawApiKey = process.env.APPLE_API_KEY + if (!rawApiKey || !keyId || !issuer) { + console.log( + 'Skipping notarization: APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER are not fully configured.' + ) + return + } + + const { keyPath, cleanup } = resolveApiKeyPath(rawApiKey) + const zipPath = path.join(appOutDir, `${appName}.zip`) + try { + await run('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appPath, zipPath]) + await run('xcrun', ['notarytool', 'submit', zipPath, '--key', keyPath, '--key-id', keyId, '--issuer', issuer, '--wait']) + await run('xcrun', ['stapler', 'staple', '-v', appPath]) + } finally { + try { + fs.rmSync(zipPath, { force: true }) + } catch { + // Best-effort cleanup. + } + cleanup() + } +} diff --git a/website/docs/getting-started/installation.md b/website/docs/getting-started/installation.md index 5ff5489f874..a5e5dd386f9 100644 --- a/website/docs/getting-started/installation.md +++ b/website/docs/getting-started/installation.md @@ -10,6 +10,15 @@ Get Hermes Agent up and running in under two minutes with the one-line installer ## Quick Install +### Desktop App (macOS + Windows) + +Prefer a native installer? Use the desktop release channel that matches your risk tolerance: + +- **Stable (recommended):** [GitHub Releases](https://github.com/NousResearch/hermes-agent/releases/latest) +- **Nightly (rolling prerelease):** [desktop-nightly](https://github.com/NousResearch/hermes-agent/releases/tag/desktop-nightly) + +Stable desktop builds ship signed/notarized macOS artifacts and Windows installers with checksum files. + ### Linux / macOS / WSL2 ```bash @@ -34,7 +43,9 @@ The installer detects Termux automatically and switches to a tested Android flow If you want the fully explicit path, follow the dedicated [Termux guide](./termux.md). :::warning Windows -Native Windows is **not supported**. Please install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) and run Hermes Agent from there. The install command above works inside WSL2. +Native Windows for the **CLI installer path** is still not supported. Please install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) and run Hermes Agent from there if you want the CLI flow. + +For native Windows, use the desktop installers from [GitHub Releases](https://github.com/NousResearch/hermes-agent/releases/latest). ::: ### What the Installer Does diff --git a/website/docs/getting-started/quickstart.md b/website/docs/getting-started/quickstart.md index 01c5239a110..084b9894aac 100644 --- a/website/docs/getting-started/quickstart.md +++ b/website/docs/getting-started/quickstart.md @@ -40,6 +40,11 @@ Run the one-line installer: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash ``` +Prefer native installers for desktop use? + +- **Stable desktop downloads:** [GitHub Releases](https://github.com/NousResearch/hermes-agent/releases/latest) +- **Nightly desktop channel:** [desktop-nightly](https://github.com/NousResearch/hermes-agent/releases/tag/desktop-nightly) + :::tip Android / Termux If you're installing on a phone, see the dedicated [Termux guide](./termux.md) for the tested manual path, supported extras, and current Android-specific limitations. ::: diff --git a/website/docs/index.md b/website/docs/index.md index db7106d9552..5baa3cc4997 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -13,6 +13,7 @@ The self-improving AI agent built by [Nous Research](https://nousresearch.com). @@ -25,6 +26,8 @@ It's not a coding copilot tethered to an IDE or a chatbot wrapper around a singl | | | |---|---| | πŸš€ **[Installation](/docs/getting-started/installation)** | Install in 60 seconds on Linux, macOS, or WSL2 | +| πŸ’» **[Desktop Downloads](https://github.com/NousResearch/hermes-agent/releases/latest)** | Signed macOS and Windows installers from GitHub Releases (stable channel) | +| πŸŒ™ **[Desktop Nightly](https://github.com/NousResearch/hermes-agent/releases/tag/desktop-nightly)** | Rolling prerelease builds from `main` for early testing | | πŸ“– **[Quickstart Tutorial](/docs/getting-started/quickstart)** | Your first conversation and key features to try | | πŸ—ΊοΈ **[Learning Path](/docs/getting-started/learning-path)** | Find the right docs for your experience level | | βš™οΈ **[Configuration](/docs/user-guide/configuration)** | Config file, providers, models, and options | diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 551242b758a..282f44e0086 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -104,6 +104,11 @@ const config: Config = { label: 'Skills', position: 'left', }, + { + href: 'https://github.com/NousResearch/hermes-agent/releases/latest', + label: 'Download', + position: 'left', + }, { href: 'https://hermes-agent.nousresearch.com', label: 'Home', @@ -144,6 +149,8 @@ const config: Config = { { title: 'More', items: [ + { label: 'Download (Stable)', href: 'https://github.com/NousResearch/hermes-agent/releases/latest' }, + { label: 'Desktop Nightly', href: 'https://github.com/NousResearch/hermes-agent/releases/tag/desktop-nightly' }, { label: 'GitHub', href: 'https://github.com/NousResearch/hermes-agent' }, { label: 'Nous Research', href: 'https://nousresearch.com' }, ],