chore(release): migrate plugin and cli tag tracks

This commit is contained in:
Bailey Dixon
2026-06-16 10:50:32 -04:00
parent 2376ee64b9
commit e7fb1dc1de
32 changed files with 478 additions and 414 deletions
+1
View File
@@ -0,0 +1 @@
*.sh text eol=lf
+4 -2
View File
@@ -5,12 +5,14 @@ on:
branches: [main, dev]
paths:
- "plugin/dashboard/**"
- "scripts/check-plugin-version-sync.py"
- "scripts/check-server-version-sync.py"
- ".github/workflows/ci-dashboard.yml"
pull_request:
branches: [main, dev]
paths:
- "plugin/dashboard/**"
- "scripts/check-plugin-version-sync.py"
- "scripts/check-server-version-sync.py"
- ".github/workflows/ci-dashboard.yml"
@@ -49,8 +51,8 @@ jobs:
with:
python-version: "3.11"
- name: Verify server-owned version metadata
run: python scripts/check-server-version-sync.py
- name: Verify plugin-owned version metadata
run: python scripts/check-plugin-version-sync.py
- name: Install dashboard API test deps
run: pip install -r relay_server/requirements.txt fastapi httpx pytest requests
@@ -1,12 +1,12 @@
# Hermes-Relay — Python Server CI Pipeline
# Hermes-Relay — Plugin CI Pipeline
#
# Runs on pushes to main/dev and on PRs targeting main/dev, scoped to
# server-affecting paths so Android-only changes don't spin up the
# plugin-affecting paths so Android-only changes don't spin up the
# Python toolchain.
#
# Pipeline: syntax-check -> focused server tests
# Pipeline: syntax-check -> focused plugin tests
name: CI — Server
name: CI — Plugin
on:
push:
@@ -26,9 +26,11 @@ on:
- "relay_server/**"
- "hermes_relay_bootstrap/**"
- "pyproject.toml"
- "scripts/check-plugin-version-sync.py"
- "scripts/check-server-version-sync.py"
- "scripts/bump-plugin-version.sh"
- "scripts/bump-server-version.sh"
- ".github/workflows/ci-server.yml"
- ".github/workflows/ci-plugin.yml"
pull_request:
branches: [main, dev]
paths:
@@ -46,18 +48,20 @@ on:
- "relay_server/**"
- "hermes_relay_bootstrap/**"
- "pyproject.toml"
- "scripts/check-plugin-version-sync.py"
- "scripts/check-server-version-sync.py"
- "scripts/bump-plugin-version.sh"
- "scripts/bump-server-version.sh"
- ".github/workflows/ci-server.yml"
- ".github/workflows/ci-plugin.yml"
# Cancel in-progress runs for the same branch/PR, but let main and dev finish
concurrency:
group: ci-server-${{ github.ref }}
group: ci-plugin-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
jobs:
# ──────────────────────────────────────────────
# Python Server — py_compile syntax sanity
# Python Plugin — py_compile syntax sanity
# ──────────────────────────────────────────────
syntax-check:
name: Syntax check (Python)
@@ -75,7 +79,7 @@ jobs:
- name: Install dependencies
run: pip install -r relay_server/requirements.txt
- name: Syntax check (server/plugin.relay — canonical location)
- name: Syntax check (plugin relay — canonical location)
run: |
python -m py_compile plugin/relay/server.py
python -m py_compile plugin/relay/channels/terminal.py
@@ -87,18 +91,18 @@ jobs:
- name: Syntax check (relay_server shim)
run: python -m py_compile relay_server/__init__.py relay_server/__main__.py
- name: Validate Server version metadata
run: python scripts/check-server-version-sync.py
- name: Validate Plugin version metadata
run: python scripts/check-plugin-version-sync.py
# ──────────────────────────────────────────────
# Python Server — focused route/auth/session tests
# Python Plugin — focused route/auth/session tests
#
# Tests are ADVISORY on dev (push or PR) so WIP commits don't block the
# merge queue. Strict on main — the dev → main release-merge PR surfaces
# any real failures before release.
# ──────────────────────────────────────────────
unit-tests:
name: Focused Server tests (Python)
name: Focused Plugin tests (Python)
needs: syntax-check
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -120,7 +124,7 @@ jobs:
pip install -r relay_server/requirements.txt
pip install pytest responses
- name: Run focused Server tests
- name: Run focused Plugin tests
run: |
python -m pytest \
plugin/tests/test_relay_security.py \
+1 -1
View File
@@ -2,7 +2,7 @@
# branch protection on `main` has a check name it can rely on, regardless
# of which paths the PR touches.
#
# Why this exists. The other CI workflows (`ci-android.yml`, `ci-server.yml`,
# Why this exists. The other CI workflows (`ci-android.yml`, `ci-plugin.yml`,
# `ci-desktop.yml`) are scoped via `paths:` filters so a docs-only or
# desktop-only PR doesn't spin up the Android toolchain. Branch protection's
# "required status checks" treat a check that doesn't run as failing — so
+1 -1
View File
@@ -3,7 +3,7 @@
# Triggered when an Android release tag (android-v*) is pushed.
# Validates the tag matches the app version in libs.versions.toml,
# runs focused Android checks, builds release APK/AAB artifacts, and creates a
# GitHub Release. Server/Python package releases use server-v* tags.
# GitHub Release. Plugin/Python package releases use plugin-v* tags.
name: Release Android
+3 -3
View File
@@ -2,7 +2,7 @@ name: Release CLI
on:
push:
tags: ['desktop-v*']
tags: ['cli-v*']
permissions:
contents: write
@@ -171,9 +171,9 @@ jobs:
- build-cli-binaries
- build-windows-tray-installer
steps:
- name: Extract desktop version
- name: Extract CLI version
id: version
run: echo "version=${GITHUB_REF_NAME#desktop-v}" >> "$GITHUB_OUTPUT"
run: echo "version=${GITHUB_REF_NAME#cli-v}" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v4
with:
+7 -7
View File
@@ -3,7 +3,7 @@ name: Release Plugin
on:
push:
tags:
- "server-v*"
- "plugin-v*"
permissions:
contents: write
@@ -20,10 +20,10 @@ jobs:
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/server-v}" >> "$GITHUB_OUTPUT"
run: echo "version=${GITHUB_REF#refs/tags/plugin-v}" >> "$GITHUB_OUTPUT"
- name: Verify Plugin version sync
run: python scripts/check-server-version-sync.py --expect "$TAG_VERSION"
run: python scripts/check-plugin-version-sync.py --expect "$TAG_VERSION"
env:
TAG_VERSION: ${{ steps.version.outputs.version }}
@@ -90,16 +90,16 @@ jobs:
uses: softprops/action-gh-release@v3
with:
name: Hermes-Relay-Plugin v${{ needs.validate.outputs.version }}
tag_name: server-v${{ needs.validate.outputs.version }}
tag_name: plugin-v${{ needs.validate.outputs.version }}
prerelease: ${{ contains(needs.validate.outputs.version, '-') }}
fail_on_unmatched_files: true
body: |
# Hermes-Relay-Plugin v${{ needs.validate.outputs.version }}
This release contains the Hermes plugin and Python package.
Android releases use `android-v*` tags. CLI releases use
`desktop-v*` tags. Historical server releases before this lane
rename used `relay-v*` tags.
Android releases use `android-v*` tags. CLI releases use `cli-v*`
tags. Historical CLI prereleases used `desktop-v*` tags, and
historical relay/plugin releases used `relay-v*` tags.
## Install
+1 -1
View File
@@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
### Changed
- **Release names normalized by surface.** Future GitHub Releases are named `Hermes-Relay-Android`, `Hermes-Relay-Plugin`, and `Hermes-Relay-CLI`, with lower-case workflow files (`release-plugin.yml`, `release-cli.yml`) while tag prefixes remain `android-v*`, `server-v*`, and `desktop-v*` for installer/update compatibility.
- **Release names normalized by surface.** Future GitHub Releases are named `Hermes-Relay-Android`, `Hermes-Relay-Plugin`, and `Hermes-Relay-CLI`, with future tags on `android-v*`, `plugin-v*`, and `cli-v*`. The CLI installer and updater still understand historical `desktop-v*` prereleases during the migration.
## [1.0.0] - 2026-06-14
+5 -5
View File
@@ -169,14 +169,14 @@ This is a **public, distributed repo** — every committed file (CHANGELOG, DEVL
- **Branching model (as of 2026-04-19):** `main` + `dev`. Feature branches target `dev`, not `main`. `main` receives only release merges (and tags). No straight-to-main exemption — even single-file typos go through `dev`.
- **Merge style:** `git merge --no-ff` — no squash. Preserves per-commit trail for agent-team branches on every merge in the chain (feature → dev → main).
- **Merging ≠ releasing.** Feature branches land on `dev` continuously as CI goes green; each PR appends to `[Unreleased]` in `CHANGELOG.md` on `dev`. Releases are a separate act — cut when accumulated state is worth shipping, not per-feature. See `RELEASE.md` "When to cut a release."
- **Version bumps happen on `dev`, then release-merge to `main`.** Bump only the surface being released: `scripts/bump-android-version.sh` for `android-vX.Y.Z`, `scripts/bump-server-version.sh` for `server-vX.Y.Z`, and `desktop/package.json` for `desktop-vX.Y.Z`. The release commit lives on `dev`, then a release PR merges `dev` → `main` with `--no-ff`, then the surface tag is cut from `main`.
- **Version bumps happen on `dev`, then release-merge to `main`.** Bump only the surface being released: `scripts/bump-android-version.sh` for `android-vX.Y.Z`, `scripts/bump-plugin-version.sh` for `plugin-vX.Y.Z`, and `desktop/package.json` for `cli-vX.Y.Z`. The release commit lives on `dev`, then a release PR merges `dev` → `main` with `--no-ff`, then the surface tag is cut from `main`.
- **Server tracks `dev` for staging.** The hermes-host deployment pulls `dev` so merged features are exercised before they reach a tag. Released state lives on tags cut from `main`.
- **Branch protection** on `main` — direct push blocked; only release-merge PRs from `dev` land here. `dev` also requires CI to pass on PRs but accepts feature-branch merges freely.
### Testing
- **Android:** JUnit + Compose testing for UI, MockK for mocks
- **Python:** `python -m unittest plugin.tests.test_<name>` — avoid bare `pytest` (conftest imports `responses` which may not be installed in the venv)
- **CI is split by path:** `.github/workflows/ci-android.yml` runs on app/Gradle changes; `.github/workflows/ci-server.yml` runs on plugin/Python changes. Both trigger on pushes to `main` and `dev` and on PRs targeting either. Build + tests must pass before merge to `dev`; release-merge to `main` requires the same.
- **CI is split by path:** `.github/workflows/ci-android.yml` runs on app/Gradle changes; `.github/workflows/ci-plugin.yml` runs on plugin/Python changes. Both trigger on pushes to `main` and `dev` and on PRs targeting either. Build + tests must pass before merge to `dev`; release-merge to `main` requires the same.
## Key Files
@@ -408,11 +408,11 @@ must not depend on this hook.
See [RELEASE.md](RELEASE.md) for the full recipe.
- **Android version source:** `gradle/libs.versions.toml` (`appVersionName`, `appVersionCode`); bump with `scripts/bump-android-version.sh`
- **Relay plugin/server version source:** `pyproject.toml`; keep plugin/dashboard metadata synced with `scripts/check-server-version-sync.py`; bump with `scripts/bump-server-version.sh`
- **Relay plugin version source:** `pyproject.toml`; keep plugin/dashboard metadata synced with `scripts/check-plugin-version-sync.py`; bump with `scripts/bump-plugin-version.sh`
- **Desktop CLI version source:** `desktop/package.json`; regenerate `desktop/src/version.ts` with `npm run gen:version`
- **Track audit:** `python scripts/check-version-tracks.py` reports Android, server/plugin, and desktop CLI versions without forcing them to match
- **Track audit:** `python scripts/check-version-tracks.py` reports Android, plugin, and CLI versions without forcing them to match
- **`appVersionCode` is monotonic** — always increment across Android prereleases
- **Cut a release:** bump the target surface → commit → merge `dev` to `main` → tag with `android-v*`, `server-v*`, or `desktop-v*` → push tag → CI builds + GitHub Release
- **Cut a release:** bump the target surface → commit → merge `dev` to `main` → tag with `android-v*`, `plugin-v*`, or `cli-v*` → push tag → CI builds + GitHub Release
- **Required secrets:** `HERMES_KEYSTORE_BASE64`, `HERMES_KEYSTORE_PASSWORD`, `HERMES_KEY_ALIAS`, `HERMES_KEY_PASSWORD`
## Integration Points
+3 -3
View File
@@ -181,7 +181,7 @@ hermes-relay daemon # headless tool router — agent
hermes-relay update # self-update via GitHub Releases
```
It pairs against the **same relay and credential store** as the Android app — pair once from either, both work. Tagged on a separate `desktop-v*` [release track](https://github.com/Codename-11/hermes-relay/releases?q=desktop).
It pairs against the **same relay and credential store** as the Android app — pair once from either, both work. Tagged on a separate `cli-v*` [release track](https://github.com/Codename-11/hermes-relay/releases?q=cli), with old alpha prereleases still visible under `desktop-v*`.
- **Docs:** [CLI guide](https://codename-11.github.io/hermes-relay/desktop/) · [`desktop/README.md`](desktop/README.md)
- **AI-agent setup recipe:** `/hermes-relay-desktop-setup`
@@ -214,7 +214,7 @@ configure API, dashboard, and relay routes without merging their auth models.
| [API Reference](https://codename-11.github.io/hermes-relay/reference/api.html) | Hermes API endpoints used by both surfaces |
| [Specification](docs/spec.md) | Full spec — protocol, UI, phases, dependencies |
| [Architecture Decisions](docs/decisions.md) | ADRs — framework, channels, auth, terminal |
| [Changelog](CHANGELOG.md) | Release history (`android-v*`, `server-v*`, `desktop-v*`) |
| [Changelog](CHANGELOG.md) | Release history (`android-v*`, `plugin-v*`, `cli-v*`) |
<details>
<summary><b>Install with an AI agent</b> — paste-ready prompt for Claude / GPT</summary>
@@ -283,7 +283,7 @@ hermes-relay/
├── user-docs/ # VitePress documentation site
├── docs/ # Spec, decisions, security
├── scripts/ # Dev helper scripts
├── .github/workflows/ # CI + release pipelines (ci-android / ci-server / ci-desktop)
├── .github/workflows/ # CI + release pipelines (ci-android / ci-plugin / ci-desktop)
└── gradle/ # Wrapper (8.13) + version catalog
```
+32 -32
View File
@@ -21,15 +21,15 @@ for automation.
| Surface | Tag prefix | Version source | Bump script | Release workflow |
|---|---|---|---|---|
| Hermes-Relay-Android | `android-v*` | `gradle/libs.versions.toml` | `scripts/bump-android-version.sh` | `.github/workflows/release-android.yml` |
| Hermes-Relay-Plugin | `server-v*` | `pyproject.toml` plus checked plugin/dashboard metadata | `scripts/bump-server-version.sh` | `.github/workflows/release-plugin.yml` |
| Hermes-Relay-CLI | `desktop-v*` | `desktop/package.json` | `npm version` or manual package bump | `.github/workflows/release-cli.yml` |
| Hermes-Relay-Plugin | `plugin-v*` | `pyproject.toml` plus checked plugin/dashboard metadata | `scripts/bump-plugin-version.sh` | `.github/workflows/release-plugin.yml` |
| Hermes-Relay-CLI | `cli-v*` | `desktop/package.json` | `npm version` or manual package bump | `.github/workflows/release-cli.yml` |
This split is intentional. The plugin carries relay features for both Android
and CLI clients, so plugin fixes can ship without forcing an Android app
`versionCode` bump, and CLI alphas can continue on their own cadence. Historical
Android releases before this naming split used bare `v*` tags, and historical
plugin/server releases used `relay-v*` tags. New releases use the explicit tag
prefixes above.
Android releases before this naming split used bare `v*` tags. Historical
plugin/server releases used `relay-v*` tags, and historical CLI prereleases used
`desktop-v*` tags. New releases use the explicit tag prefixes above.
### Android app versioning
@@ -90,13 +90,13 @@ lockstep:
Always bump Plugin releases via:
```bash
bash scripts/bump-server-version.sh 0.6.2
bash scripts/bump-plugin-version.sh 0.6.2
```
Check the current metadata with:
```bash
python scripts/check-server-version-sync.py
python scripts/check-plugin-version-sync.py
```
Check all release tracks at once with:
@@ -110,8 +110,8 @@ side by side and validates that each track's own source files are internally
consistent. It deliberately does not require all three tracks to share the same
SemVer.
The `server-v*` release workflow validates the tag against the same metadata,
runs plugin/server tests, builds a wheel and sdist, generates checksums, and
The `plugin-v*` release workflow validates the tag against the same metadata,
runs plugin tests, builds a wheel and sdist, generates checksums, and
publishes a `Hermes-Relay-Plugin vX.Y.Z` GitHub Release with the package
artifacts.
@@ -171,15 +171,15 @@ Squash merges lose that detail and are **not** the house style.
### Version bumps happen at release-prep on `dev`, NOT on feature branches
Feature branches **never** touch `gradle/libs.versions.toml`,
server-owned version metadata, or `desktop/package.json`.
plugin-owned version metadata, or `desktop/package.json`.
If two feature branches both bumped a release version, they'd collide on
version files and, for Android, on `appVersionCode` (which must be
monotonic).
Version-bump commits live on `dev` as the last commit of release-prep
work. Android commits use `release(android): android-vX.Y.Z`; server commits
use `release(server): server-vX.Y.Z`; desktop commits use the existing
`release: desktop-vX.Y.Z` convention. A release PR then merges `dev` →
work. Android commits use `release(android): android-vX.Y.Z`; plugin commits
use `release(plugin): plugin-vX.Y.Z`; CLI commits use
`release(cli): cli-vX.Y.Z`. A release PR then merges `dev` →
`main` with `--no-ff`, and the matching tag is cut from the resulting
`main` tip.
@@ -188,7 +188,7 @@ use `release(server): server-vX.Y.Z`; desktop commits use the existing
Light branch protection is enabled:
- **`main`** — direct pushes blocked; only release PRs from `dev` merge
here. PR must pass CI (Android + Server) before merge. Force push and
here. PR must pass CI (Android + Plugin) before merge. Force push and
branch deletion blocked.
- **`dev`** — direct pushes blocked for non-trivial work; feature
branches PR in. PR must pass CI. Force push and branch deletion
@@ -480,24 +480,24 @@ voice auth, dashboard plugin UI, or packaging changes.
git checkout dev
git pull --ff-only origin dev
bash scripts/bump-server-version.sh 0.6.2
bash scripts/bump-plugin-version.sh 0.6.2
git add pyproject.toml plugin/relay/__init__.py plugin/plugin.yaml plugin/dashboard/manifest.json plugin/dashboard/package.json plugin/dashboard/package-lock.json CHANGELOG.md
git commit -m "release(server): server-v0.6.2"
git commit -m "release(plugin): plugin-v0.6.2"
git push origin dev
# Open the release PR (dev -> main) and merge with --no-ff.
# After merge, tag from the new main tip:
git checkout main
git pull --ff-only origin main
git tag server-v0.6.2
git push origin server-v0.6.2
git tag plugin-v0.6.2
git push origin plugin-v0.6.2
```
Pushing `server-v*` triggers `.github/workflows/release-plugin.yml`, which
validates all server-owned version metadata with
`scripts/check-server-version-sync.py`. Run
Pushing `plugin-v*` triggers `.github/workflows/release-plugin.yml`, which
validates all plugin-owned version metadata with
`scripts/check-plugin-version-sync.py`. Run
`python scripts/check-version-tracks.py` locally before tagging when a change
touches more than one release surface. The workflow also runs server tests,
touches more than one release surface. The workflow also runs plugin tests,
builds a wheel and sdist, generates `SHA256SUMS.txt`, and creates a GitHub
Release named `Hermes-Relay-Plugin v<version>` for the plugin package.
@@ -587,9 +587,9 @@ gradlew promoteReleaseArtifact --from-track=internal --promote-track=production
## CI Behavior
Android, Server, dashboard, and desktop now have separate CI/release lanes.
Android, Plugin, dashboard, and desktop now have separate CI/release lanes.
This keeps a dashboard CSS fix from running the full server suite, and keeps
server changes from forcing an Android app `versionCode` bump.
plugin changes from forcing an Android app `versionCode` bump.
On every push of a tag matching `android-v*`, `.github/workflows/release-android.yml`:
@@ -609,18 +609,18 @@ On every push of a tag matching `android-v*`, `.github/workflows/release-android
succeeded. If `HERMES_KEYSTORE_BASE64` is missing, the summary warns
that the artifacts are debug-signed and unsuitable for Play Store.
On every push of a tag matching `server-v*`,
On every push of a tag matching `plugin-v*`,
`.github/workflows/release-plugin.yml`:
1. Validates the tag matches all server-owned version metadata checked by
`scripts/check-server-version-sync.py`.
2. Runs server syntax checks and the focused route/auth/session test slice.
1. Validates the tag matches all plugin-owned version metadata checked by
`scripts/check-plugin-version-sync.py`.
2. Runs plugin syntax checks and the focused route/auth/session test slice.
3. Builds the Python wheel and sdist with `python -m build`.
4. Generates `dist/SHA256SUMS.txt`.
5. Creates a GitHub Release named `Hermes-Relay-Plugin v<version>` with the wheel,
sdist, and checksum file attached.
On every push of a tag matching `desktop-v*`,
On every push of a tag matching `cli-v*`,
`.github/workflows/release-cli.yml` builds and publishes the CLI binaries and
Windows tray installer. Dashboard-only changes are covered by
`.github/workflows/ci-dashboard.yml`, which builds the dashboard plugin,
@@ -660,9 +660,9 @@ For an Android app hotfix:
`dev`'s `appVersionCode` lags behind `main` and the next app release
bump collides.
For a Server hotfix, branch from the affected `server-v*` tag, apply
the fix, run `bash scripts/bump-server-version.sh <next-version>`, merge to
`main`, and tag `server-v<next-version>`. Do not touch
For a Plugin hotfix, branch from the affected `plugin-v*` tag, apply
the fix, run `bash scripts/bump-plugin-version.sh <next-version>`, merge to
`main`, and tag `plugin-v<next-version>`. Do not touch
`gradle/libs.versions.toml` unless an Android app release is also shipping.
## Troubleshooting
+2 -2
View File
@@ -14,7 +14,7 @@ Native Android companion for the [Hermes agent platform](https://github.com/Nous
### Desktop track (parallel lane to Android) — **experimental**
Release tags: `desktop-v*` (separate cadence from Android `android-v*` and Plugin `server-v*`). Curl-installed prebuilt binaries (no Node required); Windows first, macOS / Linux same release. Workflows: [`ci-desktop.yml`](.github/workflows/ci-desktop.yml) + [`release-cli.yml`](.github/workflows/release-cli.yml).
Release tags: `cli-v*` (separate cadence from Android `android-v*` and Plugin `plugin-v*`). Historical alpha prereleases used `desktop-v*`, and the installer/updater keep a migration fallback. Curl-installed prebuilt binaries (no Node required); Windows first, macOS / Linux same release. Workflows: [`ci-desktop.yml`](.github/workflows/ci-desktop.yml) + [`release-cli.yml`](.github/workflows/release-cli.yml).
**Shipped (2026-04-23 — first tagged release `desktop-v0.3.0-alpha.1`):**
@@ -47,7 +47,7 @@ Release tags: `desktop-v*` (separate cadence from Android `android-v*` and Plugi
**Earlier alpha.2–alpha.5 workstreams (now in-flight / done — see DEVLOG 2026-04-23 entries for specifics):**
- **`hermes-relay update` subcommand + auto-update nudge.** The binary today does NOT self-update — users have to re-run the `curl | sh` / `irm | iex` one-liner to pick up a new release. Close the gap: `hermes-relay update` polls the GitHub Releases API, filters to `desktop-v*`, compares to `readVersion()`, and either shells out to the installer or downloads the binary directly + `rename` over the current one (Windows can rename while running; Linux/macOS atomic replace is fine for long-lived daemons because the running process keeps the old inode open). Add a once-per-day background check in `daemon` mode that emits `update_available` as a log event — opt-in via `--check-updates`, never auto-installs without user action. Signing prerequisite: SmartScreen/Gatekeeper would warn on every auto-downloaded binary until we sign, so this is behind code signing.
- **`hermes-relay update` subcommand + auto-update nudge.** The binary self-update path polls the GitHub Releases API, prefers `cli-v*`, falls back to historical `desktop-v*` prereleases during migration, compares to `readVersion()`, and downloads the binary directly + `rename` over the current one (Windows can rename while running; Linux/macOS atomic replace is fine for long-lived daemons because the running process keeps the old inode open). Add a once-per-day background check in `daemon` mode that emits `update_available` as a log event — opt-in via `--check-updates`, never auto-installs without user action. Signing prerequisite: SmartScreen/Gatekeeper would warn on every auto-downloaded binary until we sign, so this is behind code signing.
- **Workspace-awareness — desktop client sends cwd/git/hostname on connect.** Biggest lingering "is the agent working against the right tree?" problem. On WSS auth, the client advertises an ephemeral workspace descriptor — `cwd`, `git_root`, `git_branch`, `git_status_summary` (staged/modified counts), `repo_name`, `hostname`, `platform`, `active_shell`. Server-side `DesktopHandler` stashes it as live session metadata (NOT persistent state). New hermes-agent plugin hook injects a one-line ephemeral prompt prefix into the session context — *"Active desktop workspace: machine=Bailey-PC · repo=hermes-relay · branch=dev · staged=3"* — so the LLM reads it every turn without the operator having to explain. Also default `desktop_terminal` / `desktop_read_file` / `desktop_search_files` `cwd` to the repo root when unset. Expose the snapshot in `hermes-relay doctor` + `hermes-relay status` + a new `hermes-relay workspace` subcommand + a relay dashboard tab so both operator and agent have a common view. Pair with a `.hermes/workspace-context.json` file-based fallback for when the socket path can't be reached. Requires: new WSS envelope (`desktop.workspace` on connect), hermes-agent plugin hook for ephemeral context injection, schema coordination with the upstream `ContextVar` multi-client work.
- **Service installers** — `scripts/install-service-{win,linux,mac}.{ps1,sh}` — Windows Service via `sc.exe create`, `systemd --user` unit with `loginctl enable-linger`, `launchctl load` plist for macOS. Auto-start on login so the daemon is always reachable.
- **Multi-client routing on the `desktop` channel** — replace single-client MVP with per-token indexing + device-id reconnect handoff. Hermes session state carries `desktop_session_token` via a new `ContextVar` in `gateway/session_context.py` (hermes-agent PR candidate — won't affect Android). Natural pairing with the workspace-awareness envelope — the ContextVar scheme determines which client's workspace the active session sees.
+1 -1
View File
@@ -43,7 +43,7 @@ $env:HERMES_RELAY_INSTALL_SURFACE='cli'; irm https://raw.githubusercontent.com/C
curl -fsSL https://raw.githubusercontent.com/Codename-11/hermes-relay/main/desktop/scripts/install.sh | sh
```
Windows downloads and verifies `hermes-relay-desktop-windows-x64-setup.exe`, then launches the tray installer. The tray app bundles the compiled CLI sidecar so pair, daemon start/stop, devices, revoke, and task log work without a separate PATH install. CLI-only installs download the prebuilt single-file binary from GitHub Releases (Bun `--compile`, ~60–110 MB per platform) into `~/.hermes/bin/`. Pin a specific release with `HERMES_RELAY_VERSION=desktop-v0.3.0-alpha.1`; CLI-only installs can override the install dir with `HERMES_RELAY_INSTALL_DIR=...`.
Windows downloads and verifies `hermes-relay-desktop-windows-x64-setup.exe`, then launches the tray installer. The tray app bundles the compiled CLI sidecar so pair, daemon start/stop, devices, revoke, and task log work without a separate PATH install. CLI-only installs download the prebuilt single-file binary from GitHub Releases (Bun `--compile`, ~60–110 MB per platform) into `~/.hermes/bin/`. Pin a specific release with `HERMES_RELAY_VERSION=cli-v0.3.0-alpha.18`; CLI-only installs can override the install dir with `HERMES_RELAY_INSTALL_DIR=...`.
After install, use `hermes-relay <prompt>`. The shorter `hermes <prompt>` alias is optional because it can shadow a real local hermes-agent install. Enable it only when you want hermes-relay to be the `hermes` command for tools like Orca:
+16 -10
View File
@@ -6,7 +6,7 @@
# Install only the CLI binary instead:
# $env:HERMES_RELAY_INSTALL_SURFACE='cli'; irm ... | iex
# Pin a specific release:
# $env:HERMES_RELAY_VERSION='desktop-v0.3.0-alpha.1'; irm ... | iex
# $env:HERMES_RELAY_VERSION='cli-v0.3.0-alpha.18'; irm ... | iex
# Override CLI install dir:
# $env:HERMES_RELAY_INSTALL_DIR='C:\tools\hermes\bin'; irm ... | iex
# Optional `hermes` alias for Orca/upstream-style workflows:
@@ -54,7 +54,7 @@ function Read-InstalledVersion {
}
}
# Strip the `desktop-v` tag prefix to get the bare semver (KEEPS the
# Strip the `cli-v` tag prefix to get the bare semver (KEEPS the
# prerelease suffix — `0.3.0-alpha.11`, not `0.3.0`). The binary's
# `--version` output has reported the full semver since alpha.4 (when
# `gen:version` started embedding the full string from package.json), so
@@ -66,7 +66,8 @@ function Get-NormalizedPin {
param([string]$Pin)
if (-not $Pin -or $Pin -eq 'latest') { return '' }
$v = $Pin
if ($v.StartsWith('desktop-v')) { $v = $v.Substring('desktop-v'.Length) }
if ($v.StartsWith('cli-v')) { $v = $v.Substring('cli-v'.Length) }
elseif ($v.StartsWith('desktop-v')) { $v = $v.Substring('desktop-v'.Length) }
elseif ($v.StartsWith('v')) { $v = $v.Substring(1) }
return $v
}
@@ -109,12 +110,12 @@ $asset = if ($surface -eq 'tray') { "hermes-relay-desktop-windows-$arch-setup.ex
# Resolve "latest" to a concrete tag. GitHub's /releases/latest/download/ URL
# always skips prereleases, which breaks install during any all-alpha window.
# Walk the releases API (newest first) and pick the first `desktop-v*` tag,
# prerelease or not. Pinned versions (HERMES_RELAY_VERSION=desktop-v...) skip
# this and use the tag directly.
# Walk the releases API and prefer the SemVer-max `cli-v*` tag. Historical
# public prereleases used `desktop-v*`, so fall back to that track when no new
# CLI tag exists. Pinned versions skip this and use the tag directly.
$resolvedVersion = $version
if ($version -eq 'latest') {
Say "-> resolving latest desktop-v* release..."
Say "-> resolving latest cli-v* release..."
try {
$releases = Invoke-RestMethod -UseBasicParsing "https://api.github.com/repos/$repo/releases"
# Don't trust the API's first-element ordering — GitHub orders by the
@@ -125,10 +126,15 @@ if ($version -eq 'latest') {
# PRERANK is 1=alpha, 2=beta, 3=rc, 999=stable (semver §11: stable >
# any prerelease) and PRENUM is the prerelease number (so alpha.10 >
# alpha.9).
$candidates = $releases | Where-Object { $_.tag_name -like 'desktop-v*' }
if (-not $candidates) { Die "no desktop-v* releases found on $repo" }
$candidates = $releases | Where-Object { $_.tag_name -like 'cli-v*' }
if (-not $candidates) {
Say ' no cli-v* releases yet; checking historical desktop-v* prereleases...'
$candidates = $releases | Where-Object { $_.tag_name -like 'desktop-v*' }
}
if (-not $candidates) { Die "no cli-v* or historical desktop-v* releases found on $repo" }
$pick = $candidates | Sort-Object @{Expression = {
$v = $_.tag_name -replace '^desktop-v', ''
$v = $_.tag_name -replace '^cli-v', ''
$v = $v -replace '^desktop-v', ''
$core, $pre = ($v -split '-', 2)
$parts = $core -split '\.'
$major = [int]$parts[0]; $minor = [int]$parts[1]; $patch = [int]$parts[2]
+29 -19
View File
@@ -5,7 +5,7 @@
#
# Downloads a prebuilt binary from GitHub Releases — no Node.js required.
# Pin a specific release:
# HERMES_RELAY_VERSION=desktop-v0.3.0-alpha.1 curl -fsSL ... | sh
# HERMES_RELAY_VERSION=cli-v0.3.0-alpha.18 curl -fsSL ... | sh
# Override install dir:
# HERMES_RELAY_INSTALL_DIR=/opt/hermes curl -fsSL ... | sh
# Optional `hermes` alias for Orca/upstream-style workflows:
@@ -44,7 +44,7 @@ read_installed_version() {
printf '%s' "$line" | awk '{print $2}'
}
# Strip the `desktop-v` tag prefix to get the bare semver (KEEPS the
# Strip the `cli-v` tag prefix to get the bare semver (KEEPS the
# prerelease suffix — `0.3.0-alpha.11`, not `0.3.0`). The binary's
# `--version` output has reported the full semver since alpha.4 (when
# `gen:version` started embedding the full string from package.json), so
@@ -56,7 +56,8 @@ normalize_pinned_version() {
local v="$1"
# Empty or "latest" → unknown; caller decides.
[ -z "$v" ] || [ "$v" = "latest" ] && { printf ''; return 0; }
# Strip leading `desktop-v` (our release tag convention).
# Strip leading `cli-v` (current convention) or historical `desktop-v`.
v="${v#cli-v}"
v="${v#desktop-v}"
# Strip leading `v` just in case someone pinned `v0.3.0`.
v="${v#v}"
@@ -98,27 +99,36 @@ esac
# Resolve "latest" to a concrete tag. GitHub's /releases/latest/download/ URL
# always skips prereleases, which breaks install during any all-alpha window.
# Walk the releases API (newest first) and pick the first `desktop-v*` tag,
# prerelease or not. Pinned versions skip this and use the tag directly.
# Walk the releases API and prefer the SemVer-max `cli-v*` tag. Historical
# public prereleases used `desktop-v*`, so fall back to that track when no new
# CLI tag exists. Pinned versions skip this and use the tag directly.
resolved_version="$VERSION"
if [ "$VERSION" = "latest" ]; then
say "-> resolving latest desktop-v* release..."
say "-> resolving latest cli-v* release..."
api_body=$(curl -fsSL "https://api.github.com/repos/$REPO/releases" 2>/dev/null) \
|| die "could not query GitHub Releases API"
# Extract every desktop-v* tag_name and pick the SemVer-max. Don't trust
# the API's first-element ordering — GitHub orders by the release row's
# created_at which shifts when the row is edited or re-tagged, so a
# touched alpha.9 row can outrank a freshly-tagged alpha.10. `sort -V`
# handles SemVer-ish ordering well enough for our purposes; we reverse
# and take the top. Each "tag_name": entry is on its own line in
# GitHub's JSON output, so line-oriented tooling is sufficient and
# avoids a jq dependency.
resolved_version=$(printf '%s\n' "$api_body" \
| grep -E '"tag_name": *"desktop-v' \
| sed -E 's/.*"tag_name": *"([^"]+)".*/\1/' \
# Extract every CLI-track tag_name and pick the SemVer-max. Don't trust the
# API's first-element ordering — GitHub orders by the release row's created_at
# which shifts when the row is edited or re-tagged. Each "tag_name": entry is
# on its own line in GitHub's JSON output, so line-oriented tooling is
# sufficient and avoids a jq dependency.
release_tags=$(printf '%s\n' "$api_body" \
| grep -E '"tag_name": *"(cli-v|desktop-v)' \
| sed -E 's/.*"tag_name": *"([^"]+)".*/\1/' || true)
resolved_version=$(printf '%s\n' "$release_tags" \
| awk '/^cli-v/ { v=$0; sub(/^cli-v/, "", v); print v "\t" $0 }' \
| sort -V \
| tail -1)
[ -n "$resolved_version" ] || die "no desktop-v* releases found on $REPO"
| tail -1 \
| cut -f2)
if [ -z "$resolved_version" ]; then
say " no cli-v* releases yet; checking historical desktop-v* prereleases..."
resolved_version=$(printf '%s\n' "$release_tags" \
| awk '/^desktop-v/ { v=$0; sub(/^desktop-v/, "", v); print v "\t" $0 }' \
| sort -V \
| tail -1 \
| cut -f2)
fi
[ -n "$resolved_version" ] || die "no cli-v* or historical desktop-v* releases found on $REPO"
say " $resolved_version"
fi
base="https://github.com/$REPO/releases/download/$resolved_version"
+1 -1
View File
@@ -159,7 +159,7 @@ Usage:
hermes-relay devices List / revoke / extend server-side paired devices
hermes-relay daemon Run headless — expose desktop tools even when no shell is open
hermes-relay doctor Diagnostic report: version, paths, sessions, daemon status
hermes-relay update Check for and install the latest desktop-v* release
hermes-relay update Check for and install the latest cli-v* release
hermes-relay voice Show native Hermes voice config (STT/TTS/realtime providers)
hermes-relay voice mode Push-to-talk in a browser tab (proxied through this CLI)
hermes-relay workspace Print local workspace context (cwd, git, editor, shell) — --json for scripting
+3 -3
View File
@@ -143,13 +143,13 @@ export async function updateCommand(args: ParsedArgs): Promise<number> {
}
if (!info) {
// No desktop-v* releases at all — surface truthfully, don't crash.
// No CLI release-track rows at all — surface truthfully, don't crash.
if (wantJson) {
process.stdout.write(JSON.stringify(emptyReport(), null, 2) + '\n')
return 0
}
process.stdout.write(`Current version: ${VERSION}\n`)
process.stdout.write(`No desktop-v* releases found on the upstream repo.\n`)
process.stdout.write(`No cli-v* or historical desktop-v* releases found on the upstream repo.\n`)
return 0
}
@@ -170,7 +170,7 @@ export async function updateCommand(args: ParsedArgs): Promise<number> {
process.stdout.write(JSON.stringify(report, null, 2) + '\n')
return 0
}
process.stdout.write(`Up to date — you're on the latest desktop-v* release.\n`)
process.stdout.write(`Up to date — you're on the latest CLI release.\n`)
return 0
}
+25 -13
View File
@@ -3,7 +3,8 @@
//
// Strategy:
// 1. Query the GitHub Releases API for this repo (default Codename-11/hermes-relay),
// filter to tags starting with `desktop-v`, take the newest. Zero deps.
// prefer tags starting with `cli-v`, and fall back to historical
// `desktop-v` prereleases. Zero deps.
// 2. Pick the asset for this platform (`process.platform` + `process.arch`).
// 3. Download to `<target>.download`, verify against SHA256SUMS.txt, then
// atomically rename — POSIX rename keeps the running process's inode
@@ -84,9 +85,10 @@ interface ParsedVersion {
}
function parseVersion(raw: string): ParsedVersion | null {
// Strip leading `v` / `desktop-v`.
// Strip leading `v` / `cli-v` / historical `desktop-v`.
let v = raw.trim()
if (v.startsWith('desktop-v')) v = v.slice('desktop-v'.length)
if (v.startsWith('cli-v')) v = v.slice('cli-v'.length)
else if (v.startsWith('desktop-v')) v = v.slice('desktop-v'.length)
else if (v.startsWith('v')) v = v.slice(1)
const [core, pre] = v.split('-', 2)
@@ -103,6 +105,13 @@ function parseVersion(raw: string): ParsedVersion | null {
return { major, minor, patch, prerelease }
}
function releaseVersionFromTag(tag: string): string {
if (tag.startsWith('cli-v')) return tag.slice('cli-v'.length)
if (tag.startsWith('desktop-v')) return tag.slice('desktop-v'.length)
if (tag.startsWith('v')) return tag.slice(1)
return tag
}
/** Returns 1 if a > b, -1 if a < b, 0 if equal. SemVer §11 precedence rules:
* - Compare major/minor/patch numerically.
* - A version without prerelease is greater than one with prerelease.
@@ -187,19 +196,22 @@ export async function checkForUpdate(opts: { repo?: string } = {}): Promise<Upda
const repo = opts.repo ?? DEFAULT_REPO
const releases = await fetchReleases(repo)
// Filter to desktop-v* tags. NOTE: GitHub orders the response by the release
// row's created_at, NOT by SemVer of the tag — and "created_at" can shift
// when the row is touched (re-tag, manual edit). Don't trust [0]; pick the
// SemVer-maximum tag explicitly so a touched alpha.9 row can't outrank a
// freshly-tagged alpha.10. Caught by Bailey on 2026-04-25 — `update --check`
// reported "up to date" on alpha.9 because the API listed alpha.9 first.
const desktop = releases.filter((r) => r.tag_name.startsWith('desktop-v'))
if (desktop.length === 0) return null
const pick = desktop.reduce((max, r) =>
// Prefer cli-v* tags. Historical public prereleases used desktop-v*, so keep
// a fallback while the alpha channel migrates. NOTE: GitHub orders the
// response by the release row's created_at, NOT by SemVer of the tag — and
// "created_at" can shift when the row is touched (re-tag, manual edit).
// Don't trust [0]; pick the SemVer-maximum tag explicitly so a touched alpha
// row can't outrank a freshly-tagged release.
const cli = releases.filter((r) => r.tag_name.startsWith('cli-v'))
const candidates = cli.length > 0
? cli
: releases.filter((r) => r.tag_name.startsWith('desktop-v'))
if (candidates.length === 0) return null
const pick = candidates.reduce((max, r) =>
compareVersions(r.tag_name, max.tag_name) > 0 ? r : max
)
const latestVersion = pick.tag_name.slice('desktop-v'.length)
const latestVersion = releaseVersionFromTag(pick.tag_name)
const current = VERSION
const wantAsset = assetNameForPlatform()
+2 -2
View File
@@ -993,7 +993,7 @@ two operational frictions as it grew:
audience now trails `dev` by a release cadence, not a merge cadence.
**CI impact:** the same workflow files trigger on both branches via
path-filtered triggers (`ci-android.yml` / `ci-server.yml` after the
path-filtered triggers (`ci-android.yml` / `ci-plugin.yml` after the
2026-04-19 split). `docs.yml` stays `main`-only — docs publish represents
shipped state, not integration state. Surface release workflows are tag-triggered and
branch-agnostic, unchanged.
@@ -1002,7 +1002,7 @@ branch-agnostic, unchanged.
- `CLAUDE.md` — "Git" section + "Testing / CI is split by path" note
- `RELEASE.md` — "Branching policy" + Release Process step 4 + Hotfix recipe
- `CONTRIBUTING.md` — "Commit Conventions"
- `.github/workflows/ci-android.yml`, `.github/workflows/ci-server.yml`
- `.github/workflows/ci-android.yml`, `.github/workflows/ci-plugin.yml`
- `scripts/bump-version.sh` — prints the dev → main release flow
---
+4 -4
View File
@@ -36,7 +36,7 @@ relay, dashboard, Android app, desktop app, bootstrap package, or user docs.
| Surface | Upstream-supported method | Local implementation | Status | Sync check |
| --- | --- | --- | --- | --- |
| Plugin metadata and discovery | `plugin.yaml`, plugin directory discovery, `plugins.enabled`, and `register(ctx)` | `plugin/plugin.yaml`, `plugin/__init__.py` | Aligned | Keep server-owned version metadata in sync with `python scripts/check-server-version-sync.py`. |
| Plugin metadata and discovery | `plugin.yaml`, plugin directory discovery, `plugins.enabled`, and `register(ctx)` | `plugin/plugin.yaml`, `plugin/__init__.py` | Aligned | Keep plugin-owned version metadata in sync with `python scripts/check-plugin-version-sync.py`. |
| Agent tools | Tool Gateway tools registered through plugin context | `ctx.register_tool(...)` in `plugin/__init__.py`; schemas and handlers in `plugin/tools/*` | Aligned with custom transports | Tool registration should stay in `register(ctx)`; transport details stay behind handlers. |
| Dashboard tab and plugin API | Dashboard plugin manifest plus plugin API routes under the Hermes dashboard plugin mount | `plugin/dashboard/manifest.json`, `plugin/dashboard/plugin_api.py` | Aligned wrapper | Dashboard routes may proxy relay state, but discovery and mounting should stay upstream-native. |
| Chat and model API | OpenAI-compatible API server routes such as `/v1/chat/completions`, `/v1/models`, `/v1/capabilities`, `/health`, and supported streaming routes | Android `HermesApiClient`, relay docs, Web API docs | Mixed | Prefer `/v1/capabilities` when present, then targeted probes for mixed-version fallback. |
@@ -91,13 +91,13 @@ upgrading the supported Hermes baseline.
1. Re-read the upstream references at the top of this file, especially Plugins, API
server, Tool Gateway, MCP, and Voice mode.
2. Verify server-owned version metadata:
2. Verify plugin-owned version metadata:
```powershell
python scripts/check-server-version-sync.py
python scripts/check-plugin-version-sync.py
```
When Android, server/plugin, or desktop release surfaces are touched together,
When Android, plugin, or CLI release surfaces are touched together,
also run:
```powershell
+5 -5
View File
@@ -13,12 +13,12 @@ See ``plugin/relay/server.py`` for the aiohttp server,
# during its own import via ``from . import __version__``, so this ordering
# avoids a circular-import crash during package initialization.
#
# Canonical server version source is pyproject.toml's [project].version.
# Keep this runtime constant in sync with pyproject.toml for server-v*
# Canonical plugin version source is pyproject.toml's [project].version.
# Keep this runtime constant in sync with pyproject.toml for plugin-v*
# releases. Android releases use gradle/libs.versions.toml and android-v* tags;
# desktop CLI releases use desktop/package.json and desktop-v* tags. The
# /health endpoint reports this server version, and stale values make live
# server diagnosis harder than it should be.
# CLI releases use desktop/package.json and cli-v* tags. The /health endpoint
# reports this plugin version, and stale values make live diagnosis harder than
# it should be.
__version__ = "1.1.0"
from .server import create_app, main # noqa: E402 — must come after __version__
+2 -2
View File
@@ -5,8 +5,8 @@
# gradle/libs.versions.toml::appVersionName
# gradle/libs.versions.toml::appVersionCode
#
# Server/Python releases are intentionally split; use
# scripts/bump-server-version.sh for server-owned plugin/package metadata.
# Plugin/Python releases are intentionally split; use
# scripts/bump-plugin-version.sh for plugin/package metadata.
set -euo pipefail
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# scripts/bump-plugin-version.sh - bump plugin/Python package version.
#
# Updates the plugin version sources:
# pyproject.toml::project.version
# plugin/relay/__init__.py::__version__
# plugin/plugin.yaml::version
# plugin/dashboard/manifest.json::version
# plugin/dashboard/package.json::version
# plugin/dashboard/package-lock.json::version
#
# Android releases are intentionally split; use
# scripts/bump-android-version.sh for gradle/libs.versions.toml.
set -euo pipefail
if [ $# -ne 1 ]; then
echo "usage: $0 <new-version>" >&2
echo " e.g. $0 0.6.2" >&2
echo " e.g. $0 0.7.0-rc.1" >&2
exit 2
fi
NEW_VERSION="$1"
if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo " [x] '$NEW_VERSION' is not valid SemVer (MAJOR.MINOR.PATCH[-prerelease])" >&2
exit 2
fi
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
PYPROJECT="pyproject.toml"
INITPY="plugin/relay/__init__.py"
PLUGIN_YAML="plugin/plugin.yaml"
DASH_MANIFEST="plugin/dashboard/manifest.json"
DASH_PACKAGE="plugin/dashboard/package.json"
DASH_LOCK="plugin/dashboard/package-lock.json"
VERSION_CHECK="scripts/check-plugin-version-sync.py"
for f in "$PYPROJECT" "$INITPY" "$PLUGIN_YAML" "$DASH_MANIFEST" "$DASH_PACKAGE" "$DASH_LOCK" "$VERSION_CHECK"; do
[ -f "$f" ] || { echo " [x] missing $f - are you in the repo root?" >&2; exit 1; }
done
OLD_PY="$(grep -E '^version *= *"' "$PYPROJECT" | head -1 | sed -E 's/.*"(.*)".*/\1/')"
OLD_INIT="$(grep -E '^__version__ *= *"' "$INITPY" | sed -E 's/.*"(.*)".*/\1/')"
if [ -z "$OLD_PY" ] || [ -z "$OLD_INIT" ]; then
echo " [x] could not parse current plugin version sources" >&2
exit 1
fi
python "$VERSION_CHECK"
echo ""
echo " Bumping plugin version:"
echo " pyproject.toml: $OLD_PY -> $NEW_VERSION"
echo " plugin/relay version: $OLD_INIT -> $NEW_VERSION"
echo " plugin + dashboard: $OLD_PY -> $NEW_VERSION"
echo ""
python - "$NEW_VERSION" <<'PY'
from __future__ import annotations
import json
import pathlib
import re
import sys
new_version = sys.argv[1]
root = pathlib.Path(".")
def replace_regex(path: str, pattern: str, replacement: str) -> None:
file_path = root / path
text = file_path.read_text(encoding="utf-8")
updated, count = re.subn(pattern, replacement, text, count=1, flags=re.MULTILINE)
if count != 1:
raise SystemExit(f"failed to update version in {path}")
file_path.write_text(updated, encoding="utf-8")
def update_json_version(path: str, *, package_lock: bool = False) -> None:
file_path = root / path
data = json.loads(file_path.read_text(encoding="utf-8"))
data["version"] = new_version
if package_lock:
data["packages"][""]["version"] = new_version
file_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
replace_regex("pyproject.toml", r'^version *= *"[^"]*"', f'version = "{new_version}"')
replace_regex("plugin/relay/__init__.py", r'^__version__ *= *"[^"]*"', f'__version__ = "{new_version}"')
replace_regex("plugin/plugin.yaml", r"^version:\s*[^\s#]+", f"version: {new_version}")
update_json_version("plugin/dashboard/manifest.json")
update_json_version("plugin/dashboard/package.json")
update_json_version("plugin/dashboard/package-lock.json", package_lock=True)
PY
python "$VERSION_CHECK" --expect "$NEW_VERSION"
echo " [ok] Plugin version is now $NEW_VERSION"
echo ""
echo " --- diff ------------------------------------------------"
git diff --no-color "$PYPROJECT" "$INITPY" "$PLUGIN_YAML" "$DASH_MANIFEST" "$DASH_PACKAGE" "$DASH_LOCK" || true
echo " --------------------------------------------------------"
echo ""
echo " Next steps:"
echo " 1. Update CHANGELOG.md / plugin release notes if needed"
echo " 2. Commit on dev:"
echo " git add $PYPROJECT $INITPY $PLUGIN_YAML $DASH_MANIFEST $DASH_PACKAGE $DASH_LOCK CHANGELOG.md"
echo " git commit -m \"release(plugin): plugin-v$NEW_VERSION\""
echo " 3. Merge dev -> main, then tag main:"
echo " git tag plugin-v$NEW_VERSION"
echo " git push origin plugin-v$NEW_VERSION"
+3 -3
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Backward-compatible wrapper for the renamed server release helper.
# Backward-compatible wrapper for the renamed plugin release helper.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "NOTE: scripts/bump-relay-version.sh is deprecated; use scripts/bump-server-version.sh."
exec bash "$REPO_ROOT/scripts/bump-server-version.sh" "$@"
echo "NOTE: scripts/bump-relay-version.sh is deprecated; use scripts/bump-plugin-version.sh."
exec bash "$REPO_ROOT/scripts/bump-plugin-version.sh" "$@"
+3 -111
View File
@@ -1,116 +1,8 @@
#!/usr/bin/env bash
# scripts/bump-server-version.sh - bump server/Python package version.
#
# Updates the server version sources:
# pyproject.toml::project.version
# plugin/relay/__init__.py::__version__
# plugin/plugin.yaml::version
# plugin/dashboard/manifest.json::version
# plugin/dashboard/package.json::version
# plugin/dashboard/package-lock.json::version
#
# Android releases are intentionally split; use
# scripts/bump-android-version.sh for gradle/libs.versions.toml.
# Backward-compatible wrapper for the renamed plugin release helper.
set -euo pipefail
if [ $# -ne 1 ]; then
echo "usage: $0 <new-version>" >&2
echo " e.g. $0 0.6.2" >&2
echo " e.g. $0 0.7.0-rc.1" >&2
exit 2
fi
NEW_VERSION="$1"
if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo " [x] '$NEW_VERSION' is not valid SemVer (MAJOR.MINOR.PATCH[-prerelease])" >&2
exit 2
fi
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
PYPROJECT="pyproject.toml"
INITPY="plugin/relay/__init__.py"
PLUGIN_YAML="plugin/plugin.yaml"
DASH_MANIFEST="plugin/dashboard/manifest.json"
DASH_PACKAGE="plugin/dashboard/package.json"
DASH_LOCK="plugin/dashboard/package-lock.json"
VERSION_CHECK="scripts/check-server-version-sync.py"
for f in "$PYPROJECT" "$INITPY" "$PLUGIN_YAML" "$DASH_MANIFEST" "$DASH_PACKAGE" "$DASH_LOCK" "$VERSION_CHECK"; do
[ -f "$f" ] || { echo " [x] missing $f - are you in the repo root?" >&2; exit 1; }
done
OLD_PY="$(grep -E '^version *= *"' "$PYPROJECT" | head -1 | sed -E 's/.*"(.*)".*/\1/')"
OLD_INIT="$(grep -E '^__version__ *= *"' "$INITPY" | sed -E 's/.*"(.*)".*/\1/')"
if [ -z "$OLD_PY" ] || [ -z "$OLD_INIT" ]; then
echo " [x] could not parse current server version sources" >&2
exit 1
fi
python "$VERSION_CHECK"
echo ""
echo " Bumping server version:"
echo " pyproject.toml: $OLD_PY -> $NEW_VERSION"
echo " plugin/relay version: $OLD_INIT -> $NEW_VERSION"
echo " plugin + dashboard: $OLD_PY -> $NEW_VERSION"
echo ""
python - "$NEW_VERSION" <<'PY'
from __future__ import annotations
import json
import pathlib
import re
import sys
new_version = sys.argv[1]
root = pathlib.Path(".")
def replace_regex(path: str, pattern: str, replacement: str) -> None:
file_path = root / path
text = file_path.read_text(encoding="utf-8")
updated, count = re.subn(pattern, replacement, text, count=1, flags=re.MULTILINE)
if count != 1:
raise SystemExit(f"failed to update version in {path}")
file_path.write_text(updated, encoding="utf-8")
def update_json_version(path: str, *, package_lock: bool = False) -> None:
file_path = root / path
data = json.loads(file_path.read_text(encoding="utf-8"))
data["version"] = new_version
if package_lock:
data["packages"][""]["version"] = new_version
file_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
replace_regex("pyproject.toml", r'^version *= *"[^"]*"', f'version = "{new_version}"')
replace_regex("plugin/relay/__init__.py", r'^__version__ *= *"[^"]*"', f'__version__ = "{new_version}"')
replace_regex("plugin/plugin.yaml", r"^version:\s*[^\s#]+", f"version: {new_version}")
update_json_version("plugin/dashboard/manifest.json")
update_json_version("plugin/dashboard/package.json")
update_json_version("plugin/dashboard/package-lock.json", package_lock=True)
PY
python "$VERSION_CHECK" --expect "$NEW_VERSION"
echo " [ok] Server version is now $NEW_VERSION"
echo ""
echo " --- diff ------------------------------------------------"
git diff --no-color "$PYPROJECT" "$INITPY" "$PLUGIN_YAML" "$DASH_MANIFEST" "$DASH_PACKAGE" "$DASH_LOCK" || true
echo " --------------------------------------------------------"
echo ""
echo " Next steps:"
echo " 1. Update CHANGELOG.md / server release notes if needed"
echo " 2. Commit on dev:"
echo " git add $PYPROJECT $INITPY $PLUGIN_YAML $DASH_MANIFEST $DASH_PACKAGE $DASH_LOCK CHANGELOG.md"
echo " git commit -m \"release(server): server-v$NEW_VERSION\""
echo " 3. Merge dev -> main, then tag main:"
echo " git tag server-v$NEW_VERSION"
echo " git push origin server-v$NEW_VERSION"
echo "NOTE: scripts/bump-server-version.sh is deprecated; use scripts/bump-plugin-version.sh."
exec bash "$REPO_ROOT/scripts/bump-plugin-version.sh" "$@"
+4 -4
View File
@@ -3,18 +3,18 @@
#
# Hermes-Relay now has split release tracks:
# - Android app: android-vX.Y.Z, version in gradle/libs.versions.toml
# - Server/Python package: server-vX.Y.Z, version in pyproject.toml
# - Plugin/Python package: plugin-vX.Y.Z, version in pyproject.toml
# and plugin/relay/__init__.py
# - Desktop CLI: desktop-vX.Y.Z, version in desktop/package.json
# - CLI: cli-vX.Y.Z, version in desktop/package.json
#
# Keep this legacy script as an alias for the Android app bump so older release
# notes and muscle memory still work, but prefer the explicit script names:
# scripts/bump-android-version.sh X.Y.Z
# scripts/bump-server-version.sh X.Y.Z
# scripts/bump-plugin-version.sh X.Y.Z
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo "NOTE: scripts/bump-version.sh is now an Android app alias."
echo " Use scripts/bump-server-version.sh for server-v* releases."
echo " Use scripts/bump-plugin-version.sh for plugin-v* releases."
exec bash "$REPO_ROOT/scripts/bump-android-version.sh" "$@"
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""Verify plugin-owned version metadata stays in sync.
Plugin releases use the `plugin-v*` track. The canonical version is
`pyproject.toml`'s `[project].version`; plugin and dashboard metadata should
match because they ship as one Hermes plugin package.
Android and CLI versions are intentionally separate release tracks and are
not checked here.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import re
import sys
import tomllib
from collections.abc import Callable
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
def _read_text(rel_path: str) -> str:
return (REPO_ROOT / rel_path).read_text(encoding="utf-8")
def _read_json(rel_path: str) -> object:
return json.loads(_read_text(rel_path))
def _pyproject_version() -> str:
data = tomllib.loads(_read_text("pyproject.toml"))
return str(data["project"]["version"])
def _regex_version(rel_path: str, pattern: str) -> str:
text = _read_text(rel_path)
match = re.search(pattern, text, re.MULTILINE)
if not match:
raise ValueError(f"{rel_path} has no parseable version")
return match.group(1)
def _json_version(rel_path: str, accessor: Callable[[object], object]) -> str:
value = accessor(_read_json(rel_path))
if not isinstance(value, str) or not value:
raise ValueError(f"{rel_path} has no parseable version")
return value
def _root_package_lock_version(data: object) -> object:
if not isinstance(data, dict):
raise ValueError("package-lock root is not an object")
packages = data.get("packages")
if not isinstance(packages, dict):
raise ValueError("package-lock has no packages object")
root_pkg = packages.get("")
if not isinstance(root_pkg, dict):
raise ValueError("package-lock has no root package entry")
return root_pkg.get("version")
def collect_versions() -> list[tuple[str, str]]:
return [
("pyproject.toml", _pyproject_version()),
(
"plugin/relay/__init__.py",
_regex_version(
"plugin/relay/__init__.py",
r'^__version__\s*=\s*"([^"]+)"',
),
),
(
"plugin/plugin.yaml",
_regex_version("plugin/plugin.yaml", r"^version:\s*([^\s#]+)"),
),
(
"plugin/dashboard/manifest.json",
_json_version(
"plugin/dashboard/manifest.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
"plugin/dashboard/package.json",
_json_version(
"plugin/dashboard/package.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
"plugin/dashboard/package-lock.json",
_json_version(
"plugin/dashboard/package-lock.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
'plugin/dashboard/package-lock.json packages[""]',
_json_version(
"plugin/dashboard/package-lock.json",
_root_package_lock_version,
),
),
]
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate plugin/dashboard version metadata."
)
parser.add_argument(
"--expect",
help="Require all plugin-owned metadata to match this exact version.",
)
args = parser.parse_args()
try:
versions = collect_versions()
except Exception as exc:
print(f"version sync check failed: {exc}", file=sys.stderr)
return 1
canonical = versions[0][1]
if args.expect and canonical != args.expect:
print(
f"pyproject.toml version {canonical!r} does not match expected "
f"{args.expect!r}",
file=sys.stderr,
)
return 1
width = max(len(label) for label, _ in versions)
for label, version in versions:
print(f"{label:<{width}} {version}")
mismatches = [(label, version) for label, version in versions if version != canonical]
if mismatches:
print(
f"plugin version metadata is out of sync; expected {canonical}",
file=sys.stderr,
)
for label, version in mismatches:
print(f" {label}: {version}", file=sys.stderr)
return 1
print(f"plugin version metadata is in sync: {canonical}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Backward-compatible wrapper for scripts/check-server-version-sync.py."""
"""Backward-compatible wrapper for scripts/check-plugin-version-sync.py."""
from __future__ import annotations
@@ -9,13 +9,13 @@ import sys
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
TARGET = REPO_ROOT / "scripts" / "check-server-version-sync.py"
TARGET = REPO_ROOT / "scripts" / "check-plugin-version-sync.py"
if __name__ == "__main__":
print(
"NOTE: scripts/check-relay-version-sync.py is deprecated; "
"use scripts/check-server-version-sync.py.",
"use scripts/check-plugin-version-sync.py.",
file=sys.stderr,
)
runpy.run_path(str(TARGET), run_name="__main__")
+9 -143
View File
@@ -1,155 +1,21 @@
#!/usr/bin/env python3
"""Verify server-owned version metadata stays in sync.
Server releases use the `server-v*` track. The canonical version is
`pyproject.toml`'s `[project].version`; plugin and dashboard metadata should
match because they ship as one Hermes plugin package.
Android and desktop versions are intentionally separate release tracks and are
not checked here.
"""
"""Backward-compatible wrapper for scripts/check-plugin-version-sync.py."""
from __future__ import annotations
import argparse
import json
import pathlib
import re
import runpy
import sys
import tomllib
from collections.abc import Callable
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
def _read_text(rel_path: str) -> str:
return (REPO_ROOT / rel_path).read_text(encoding="utf-8")
def _read_json(rel_path: str) -> object:
return json.loads(_read_text(rel_path))
def _pyproject_version() -> str:
data = tomllib.loads(_read_text("pyproject.toml"))
return str(data["project"]["version"])
def _regex_version(rel_path: str, pattern: str) -> str:
text = _read_text(rel_path)
match = re.search(pattern, text, re.MULTILINE)
if not match:
raise ValueError(f"{rel_path} has no parseable version")
return match.group(1)
def _json_version(rel_path: str, accessor: Callable[[object], object]) -> str:
value = accessor(_read_json(rel_path))
if not isinstance(value, str) or not value:
raise ValueError(f"{rel_path} has no parseable version")
return value
def _root_package_lock_version(data: object) -> object:
if not isinstance(data, dict):
raise ValueError("package-lock root is not an object")
packages = data.get("packages")
if not isinstance(packages, dict):
raise ValueError("package-lock has no packages object")
root_pkg = packages.get("")
if not isinstance(root_pkg, dict):
raise ValueError("package-lock has no root package entry")
return root_pkg.get("version")
def collect_versions() -> list[tuple[str, str]]:
return [
("pyproject.toml", _pyproject_version()),
(
"plugin/relay/__init__.py",
_regex_version(
"plugin/relay/__init__.py",
r'^__version__\s*=\s*"([^"]+)"',
),
),
(
"plugin/plugin.yaml",
_regex_version("plugin/plugin.yaml", r"^version:\s*([^\s#]+)"),
),
(
"plugin/dashboard/manifest.json",
_json_version(
"plugin/dashboard/manifest.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
"plugin/dashboard/package.json",
_json_version(
"plugin/dashboard/package.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
"plugin/dashboard/package-lock.json",
_json_version(
"plugin/dashboard/package-lock.json",
lambda data: data["version"], # type: ignore[index]
),
),
(
'plugin/dashboard/package-lock.json packages[""]',
_json_version(
"plugin/dashboard/package-lock.json",
_root_package_lock_version,
),
),
]
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate server/plugin/dashboard version metadata."
)
parser.add_argument(
"--expect",
help="Require all relay-owned metadata to match this exact version.",
)
args = parser.parse_args()
try:
versions = collect_versions()
except Exception as exc:
print(f"version sync check failed: {exc}", file=sys.stderr)
return 1
canonical = versions[0][1]
if args.expect and canonical != args.expect:
print(
f"pyproject.toml version {canonical!r} does not match expected "
f"{args.expect!r}",
file=sys.stderr,
)
return 1
width = max(len(label) for label, _ in versions)
for label, version in versions:
print(f"{label:<{width}} {version}")
mismatches = [(label, version) for label, version in versions if version != canonical]
if mismatches:
print(
f"server version metadata is out of sync; expected {canonical}",
file=sys.stderr,
)
for label, version in mismatches:
print(f" {label}: {version}", file=sys.stderr)
return 1
print(f"server version metadata is in sync: {canonical}")
return 0
TARGET = REPO_ROOT / "scripts" / "check-plugin-version-sync.py"
if __name__ == "__main__":
raise SystemExit(main())
print(
"NOTE: scripts/check-server-version-sync.py is deprecated; "
"use scripts/check-plugin-version-sync.py.",
file=sys.stderr,
)
runpy.run_path(str(TARGET), run_name="__main__")
+10 -10
View File
@@ -4,8 +4,8 @@
The repo ships three independently versioned artifacts:
- Android app: ``android-v*`` tags, source in ``gradle/libs.versions.toml``.
- Server/plugin: ``server-v*`` tags, source in ``pyproject.toml``.
- Desktop CLI: ``desktop-v*`` tags, source in ``desktop/package.json``.
- Plugin: ``plugin-v*`` tags, source in ``pyproject.toml``.
- CLI: ``cli-v*`` tags, source in ``desktop/package.json``.
This script gives release prep one command that checks the sources without
forcing unrelated artifacts to share the same SemVer.
@@ -87,7 +87,7 @@ def _android_track(errors: list[str]) -> Track:
)
def _server_track(errors: list[str]) -> Track:
def _plugin_track(errors: list[str]) -> Track:
pyproject = tomllib.loads(_read_text("pyproject.toml"))
project = pyproject.get("project")
if not isinstance(project, dict):
@@ -106,20 +106,20 @@ def _server_track(errors: list[str]) -> Track:
('plugin/dashboard/package-lock.json packages[""]', _package_lock_root_version()),
]
if not SEMVER_RE.match(version):
errors.append(f"server/plugin version is not SemVer: {version!r}")
errors.append(f"plugin version is not SemVer: {version!r}")
for source, found in versions:
if found != version:
errors.append(f"server/plugin version mismatch: {source} has {found}, expected {version}")
errors.append(f"plugin version mismatch: {source} has {found}, expected {version}")
return Track(
name="plugin",
version=version,
source="pyproject.toml",
tag=f"server-v{version}",
tag=f"plugin-v{version}",
details="plugin + dashboard metadata",
)
def _desktop_track(errors: list[str]) -> Track:
def _cli_track(errors: list[str]) -> Track:
version = _json_version("desktop/package.json")
generated = _regex_version("desktop/src/version.ts", r'^export const VERSION = "([^"]+)" as const')
if not SEMVER_RE.match(version):
@@ -133,7 +133,7 @@ def _desktop_track(errors: list[str]) -> Track:
name="cli",
version=version,
source="desktop/package.json",
tag=f"desktop-v{version}",
tag=f"cli-v{version}",
details="src/version.ts generated",
)
@@ -142,8 +142,8 @@ def collect_tracks() -> tuple[list[Track], list[str]]:
errors: list[str] = []
tracks = [
_android_track(errors),
_server_track(errors),
_desktop_track(errors),
_plugin_track(errors),
_cli_track(errors),
]
return tracks, errors
+2 -2
View File
@@ -11,7 +11,7 @@ It also includes a terminal escape hatch for when *you* want to drive: bare `her
:::
::: info Where this track is headed
This surface is focusing into a **remote-hands connector** — remote control, filesystem, and terminal access for the agent on machines you install it to. Desktop chat and management UX belong to [hermes-desktop](https://github.com/NousResearch/hermes-agent); this CLI's `chat` mode keeps working for scripting but isn't where new features land. "Desktop" is shorthand, not a constraint — the same binary runs on laptops and headless servers (`daemon` mode needs no display at all), and the track naming will be reframed to match in a later refactor.
This surface is focusing into a **remote-hands connector** — remote control, filesystem, and terminal access for the agent on machines you install it to. Desktop chat and management UX belong to [hermes-desktop](https://github.com/NousResearch/hermes-agent); this CLI's `chat` mode keeps working for scripting but isn't where new features land. "Desktop" is shorthand, not a constraint — the same binary runs on laptops and headless servers (`daemon` mode needs no display at all). New release tags use the `cli-v*` track; historical alpha prereleases used `desktop-v*`.
:::
## The point — the agent works on *your* machine
@@ -129,4 +129,4 @@ Use `shell` when you want to drive interactively; use `chat --json` from scripts
- [Hermes Agent](https://github.com/NousResearch/hermes-agent) — the agent platform the CLI talks to.
- [Herm](https://github.com/liftaris/herm) — OpenTUI dashboard plugin installable from the desktop surface.
- [CLI GitHub source](https://github.com/Codename-11/hermes-relay/tree/main/desktop) — `@hermes-relay/cli` package.
- [Release notes](https://github.com/Codename-11/hermes-relay/releases?q=desktop) — tagged `desktop-v*` (separate track from Android).
- [Release notes](https://github.com/Codename-11/hermes-relay/releases?q=cli) — tagged `cli-v*` (separate track from Android); old alpha prereleases are under `desktop-v*`.
+7 -7
View File
@@ -18,7 +18,7 @@ irm https://raw.githubusercontent.com/Codename-11/hermes-relay/main/desktop/scri
The script:
1. Detects architecture (x64; ARM64 lands once Bun's cross-compile target stabilizes).
2. Resolves the **latest** desktop release by querying the GitHub Releases API directly and picking the SemVer-max `desktop-v*` tag — prereleases included, so alpha builds aren't skipped (see CHANGELOG entry on alpha.11 for why this matters).
2. Resolves the **latest** CLI release by querying the GitHub Releases API directly and picking the SemVer-max `cli-v*` tag, with a migration fallback to historical `desktop-v*` prereleases. Prereleases are included, so alpha builds aren't skipped (see CHANGELOG entry on alpha.11 for why this matters).
3. Downloads `hermes-relay-win-x64.exe` and verifies SHA256 against the published `SHA256SUMS.txt`.
4. Reads the existing binary's `--version` (if present) and prints one of:
- `existing install detected: 0.3.0-alpha.17 — upgrading to 0.3.0-alpha.18`
@@ -49,11 +49,11 @@ Code signing (EV cert) is a v1.0 milestone — the experimental phase doesn't ju
### Pin a specific version
```powershell
$env:HERMES_RELAY_VERSION = 'desktop-v0.3.0-alpha.18'
$env:HERMES_RELAY_VERSION = 'cli-v0.3.0-alpha.18'
irm https://raw.githubusercontent.com/Codename-11/hermes-relay/main/desktop/scripts/install.ps1 | iex
```
The version-aware readback compares full SemVer including the prerelease tail, so `desktop-v0.3.0-alpha.17` → `desktop-v0.3.0-alpha.18` is recognized as an upgrade rather than a reinstall.
The version-aware readback compares full SemVer including the prerelease tail, so `cli-v0.3.0-alpha.17` → `cli-v0.3.0-alpha.18` is recognized as an upgrade rather than a reinstall.
### Uninstall
@@ -72,7 +72,7 @@ curl -fsSL https://raw.githubusercontent.com/Codename-11/hermes-relay/main/deskt
The script:
1. Detects OS/arch (supports `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`).
2. Resolves the latest `desktop-v*` release via the Releases API + `sort -V` (prerelease-aware, no shell deps beyond `curl` / `sort`).
2. Resolves the latest `cli-v*` release via the Releases API + `sort -V`, with a migration fallback to historical `desktop-v*` prereleases (prerelease-aware, no shell deps beyond `curl` / `sort`).
3. Downloads the matching binary + `SHA256SUMS.txt` and verifies SHA256 (`sha256sum` on Linux, `shasum -a 256` on macOS).
4. Reads the existing binary's `--version` if present and prints `upgrading X → Y` / `reinstalling X` / `installing fresh`.
5. Installs to `$HOME/.hermes/bin/hermes-relay` (mode 0755).
@@ -107,7 +107,7 @@ Apple Developer ID signing + notarization is a v1.0 milestone.
### Pin a specific version
```bash
HERMES_RELAY_VERSION=desktop-v0.3.0-alpha.18 \
HERMES_RELAY_VERSION=cli-v0.3.0-alpha.18 \
curl -fsSL https://raw.githubusercontent.com/Codename-11/hermes-relay/main/desktop/scripts/install.sh | sh
```
@@ -120,7 +120,7 @@ See [Uninstall](#uninstall) below — the curl one-liner reverses install.sh, wi
Once installed, you don't have to keep re-running the `curl | sh` one-liner. The binary self-updates:
```bash
hermes-relay update # download + verify + swap to latest desktop-v*
hermes-relay update # download + verify + swap to latest cli-v*
hermes-relay update --check # dry-run: print available version, don't install
hermes-relay update --yes # skip confirm prompt
hermes-relay update --json # machine-readable status
@@ -128,7 +128,7 @@ hermes-relay update --json # machine-readable status
The updater:
1. Polls the GitHub Releases API and picks the SemVer-max `desktop-v*` tag (prereleases included). The same resolver as the install scripts — fixed in alpha.11; pre-alpha.11 builds may report "Up to date" when a newer alpha exists, so use the install one-liner once to bootstrap onto alpha.11+ if you're stuck below it.
1. Polls the GitHub Releases API and picks the SemVer-max `cli-v*` tag, with a migration fallback to historical `desktop-v*` prereleases (prereleases included). The same resolver as the install scripts — fixed in alpha.11; pre-alpha.11 builds may report "Up to date" when a newer alpha exists, so use the install one-liner once to bootstrap onto alpha.11+ if you're stuck below it.
2. SemVer-compares to your running version (`hermes-relay --version` — embedded at build time, accurate inside Bun-compiled binaries).
3. Downloads the platform asset and verifies SHA256.
4. **POSIX (macOS / Linux):** atomic `fs.rename` over the running binary. The running process keeps the old inode open, so `hermes-relay daemon` (if running) keeps serving until restarted; the next `hermes-relay <verb>` invocation picks up the new binary.