diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eded282..ff673e6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -107,9 +107,63 @@ jobs:
if-no-files-found: error
retention-days: 30
+ build-desktop-macos:
+ name: build desktop macos
+ runs-on: macos-14
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.inputs.tag || github.ref }}
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Build packaged desktop app
+ run: |
+ cd apps/desktop
+ bun run clean
+ bun run package
+
+ - name: Normalize desktop artifact names
+ shell: bash
+ run: |
+ mkdir -p release-desktop
+
+ for file in apps/desktop/artifacts/stable-macos-*-AxonServoProgrammer.dmg; do
+ [[ -e "$file" ]] || continue
+ arch="${file#apps/desktop/artifacts/stable-macos-}"
+ arch="${arch%-AxonServoProgrammer.dmg}"
+ cp "$file" "release-desktop/Axon-Servo-Programmer-macos-${arch}.dmg"
+ done
+
+ for file in apps/desktop/artifacts/stable-macos-*-AxonServoProgrammer.app.tar.zst; do
+ [[ -e "$file" ]] || continue
+ arch="${file#apps/desktop/artifacts/stable-macos-}"
+ arch="${arch%-AxonServoProgrammer.app.tar.zst}"
+ cp "$file" "release-desktop/Axon-Servo-Programmer-macos-${arch}.app.tar.zst"
+ done
+
+ ls -la release-desktop/
+
+ - name: Upload desktop artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: desktop-macos
+ path: release-desktop/*
+ if-no-files-found: error
+ retention-days: 30
+
release:
name: release
- needs: build
+ needs:
+ - build
+ - build-desktop-macos
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
@@ -126,7 +180,7 @@ jobs:
- name: Flatten artifact tree
run: |
mkdir -p release
- find artifacts -type f \( -name 'axon-*' -o -name '*.sha256' \) -exec cp {} release/ \;
+ find artifacts -type f \( -name 'axon-*' -o -name '*.sha256' -o -name 'Axon-Servo-Programmer-*' \) -exec cp {} release/ \;
cp scripts/install.sh release/install.sh
ls -la release/
@@ -144,6 +198,8 @@ jobs:
axon-linux-x64.sha256
axon-windows-x64.exe
axon-windows-x64.exe.sha256
+ Axon-Servo-Programmer-macos-arm64.dmg
+ Axon-Servo-Programmer-macos-arm64.app.tar.zst
install.sh
)
@@ -202,3 +258,10 @@ jobs:
- Full install matrix: [docs/INSTALL.md](https://github.com/caryden/servo-programmer/blob/main/docs/INSTALL.md)
- Project background: [docs/the-adventure.md](https://github.com/caryden/servo-programmer/blob/main/docs/the-adventure.md)
- Homebrew, npm, and Scoop remain planned follow-on channels; direct download and `install.sh` are the v1.0 path.
+
+ ### Experimental desktop app (macOS)
+
+ - Apple Silicon macOS build: `Axon-Servo-Programmer-macos-arm64.dmg`
+ - Raw app bundle archive: `Axon-Servo-Programmer-macos-arm64.app.tar.zst`
+ - This desktop app is still an experimental side project built with Electrobun.
+ - If Gatekeeper warns on first launch, open the app via Finder with right-click → `Open`.
diff --git a/.gitignore b/.gitignore
index e51e036..567d5e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,6 +34,8 @@ node_modules/
*.log
apps/cli/bin/
apps/cli/dist/
+apps/desktop/artifacts/
+apps/desktop/build/
apps/web/dist/
apps/web/.pages/
playwright-report/
diff --git a/README.md b/README.md
index cdd781f..9200853 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,12 @@
> Cross-platform replacement for the legacy Axon Robotics Servo
> Programming Software: a released CLI, a live browser app, and a
-> desktop app built from scratch by reverse-engineering a closed HW/SW
-> system with [Claude Code](https://claude.com/claude-code), a Saleae
-> Logic 8, and Ghidra.
+> desktop side experiment built with Electrobun to prove UI/runtime
+> fidelity against the CLI and web app, and to explore a macOS app
+> packaging path, all built from scratch by reverse-engineering a
+> closed HW/SW system with
+> [Claude Code](https://claude.com/claude-code), a Saleae Logic 8, and
+> Ghidra.
>
> This started as a research experiment — *"how far can a single
> developer get with a coding agent on a reverse-engineering project
@@ -96,7 +99,33 @@ axon --version
axon status
```
-There is no desktop app installer yet. The desktop app remains a source/dev build for now.
+The desktop app was a side experiment with Electrobun. We pushed it far
+enough to prove fidelity with the CLI/web stack and to explore a macOS
+packaging path. The CLI remains the primary supported install surface;
+desktop packaging is still experimental. Tagged releases are intended
+to publish a macOS DMG alongside the CLI assets.
+
+### Experimental desktop app (macOS)
+
+Download the latest experimental desktop app from the
+[latest release](https://github.com/caryden/servo-programmer/releases/latest):
+
+- `Axon-Servo-Programmer-macos-arm64.dmg`
+
+Install path:
+
+1. download the DMG from the latest release
+2. open it and drag `Axon Servo Programmer.app` into `Applications`
+3. launch it from `Applications`
+
+Notes:
+
+- this app is still experimental
+- current packaged desktop release is macOS Apple Silicon only
+- because the app is not yet signed/notarized, macOS may require
+ `right-click -> Open` the first time
+- desktop assets are published by the tagged release workflow; merging
+ to `main` alone does not create a downloadable DMG
## Features
@@ -189,7 +218,7 @@ The repo is now organized as a Bun workspace:
- [`apps/cli/`](apps/cli/) — production `axon` CLI
- [`apps/web/`](apps/web/) — browser WebHID app
-- [`apps/desktop/`](apps/desktop/) — Electrobun desktop app
+- [`apps/desktop/`](apps/desktop/) — Electrobun desktop PoC / side experiment
- [`packages/core/`](packages/core/) — shared transport-agnostic protocol/catalog logic
- [`packages/ui/`](packages/ui/) — shared probe UI used by the browser and desktop apps
- [`packages/transport-nodehid/`](packages/transport-nodehid/) — Node/Bun HID transport
diff --git a/RELEASE.md b/RELEASE.md
index 9dd7456..a1f6cc6 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,6 +1,7 @@
# Release Process
-Maintainer-facing notes for cutting and publishing a release of `axon`.
+Maintainer-facing notes for cutting and publishing a release of `axon`
+plus the experimental macOS desktop app artifact.
`INSTALL.md` is for end users. This document is for the person shipping
the binaries.
@@ -25,15 +26,17 @@ flowchart TD
C --> D["Create annotated tag
vX.Y.Z"]
D --> E["Push tag to origin"]
E --> F["GitHub Actions: Release workflow"]
- F --> G["Build platform binaries
linux-x64, linux-arm64,
darwin-arm64, darwin-x64,
windows-x64"]
- G --> H["Upload temporary Actions artifacts"]
- H --> I["Release job downloads artifacts
and creates or updates a
draft GitHub Release"]
- I --> J["Review assets and release notes"]
- J --> K["Download one release asset
from the draft release"]
- K --> L["Verify SHA-256"]
- L --> M["Run downloaded binary
against real hardware"]
- M --> N["Publish the draft release"]
- N --> O["install.sh latest/download now points at this release"]
+ F --> G["Build CLI binaries
linux-x64, linux-arm64,
darwin-arm64, darwin-x64,
windows-x64"]
+ F --> H["Build desktop macOS artifact
DMG + raw app archive"]
+ G --> I["Upload temporary Actions artifacts"]
+ H --> I
+ I --> J["Release job downloads artifacts
and creates or updates a
draft GitHub Release"]
+ J --> K["Review assets and release notes"]
+ K --> L["Download one release asset
from the draft release"]
+ L --> M["Verify SHA-256"]
+ M --> N["Run downloaded binary or app
against real hardware"]
+ N --> O["Publish the draft release"]
+ O --> P["install.sh latest/download now points at this release"]
```
## Where the mechanics live
@@ -48,6 +51,8 @@ flowchart TD
1. Make sure `main` is the intended release commit and CI is green.
2. Update [apps/cli/package.json](apps/cli/package.json) to the release version.
+ The desktop app bundle version is derived from that file, so there is
+ no second desktop version to bump manually.
3. Update [CHANGELOG.md](CHANGELOG.md) with the dated release entry.
4. Create and push the tag:
@@ -75,6 +80,7 @@ flowchart TD
7. Confirm the draft has:
- all expected assets
- `install.sh`
+ - the experimental macOS desktop DMG
- release notes that match the corresponding `CHANGELOG.md` entry
- the project-story link to `docs/the-adventure.md`
@@ -123,13 +129,19 @@ Once published, the public release path should resolve normally under
## What the release workflow actually does
-The release workflow has two jobs:
+The release workflow has three jobs:
1. `build`
- builds the standalone binaries on the appropriate runners
- uploads them as temporary GitHub Actions artifacts
-2. `release`
+2. `build-desktop-macos`
+ - builds the experimental desktop app on `macos-14`
+ - packages a DMG plus a raw `.app.tar.zst` archive
+ - normalizes the release filenames
+ - uploads them as temporary GitHub Actions artifacts
+
+3. `release`
- downloads those temporary artifacts
- flattens them into one directory
- adds `install.sh`
@@ -143,6 +155,23 @@ Important distinction:
End users only care about the release assets.
+## Current release asset set
+
+The release currently publishes:
+
+- `axon-darwin-arm64`
+- `axon-darwin-x64`
+- `axon-linux-arm64`
+- `axon-linux-x64`
+- `axon-windows-x64.exe`
+- matching `.sha256` files
+- `install.sh`
+- `Axon-Servo-Programmer-macos-arm64.dmg`
+- `Axon-Servo-Programmer-macos-arm64.app.tar.zst`
+
+The desktop artifacts are intentionally macOS-only for now. Windows and
+Linux desktop packaging are out of scope for the current release path.
+
## Native HID packaging note
The standalone binaries intentionally embed the platform-specific
diff --git a/apps/desktop/README.md b/apps/desktop/README.md
index 40579b8..efff470 100644
--- a/apps/desktop/README.md
+++ b/apps/desktop/README.md
@@ -1,6 +1,10 @@
# Electrobun App
-This is the desktop Axon Servo Programmer built with Electrobun.
+This is the desktop Axon Servo Programmer built with Electrobun. It is
+still best understood as a side experiment: the goal was to prove that
+the shared UI and shared core logic could survive a native desktop
+packaging path, not to replace the CLI or web app as the primary
+surface.
## Why this shape
@@ -47,6 +51,24 @@ cd /Users/caryden/github/servo-programmer/apps/desktop
bun run start
```
+## Package it
+
+To build a production desktop artifact locally on macOS:
+
+```bash
+cd /Users/caryden/github/servo-programmer/apps/desktop
+bun run clean
+bun run package
+```
+
+Current stable build outputs land in:
+
+- `apps/desktop/artifacts/stable-macos-arm64-AxonServoProgrammer.dmg`
+- `apps/desktop/artifacts/stable-macos-arm64-AxonServoProgrammer.app.tar.zst`
+
+The DMG is the end-user install path. The `.app.tar.zst` archive is the
+raw packaged bundle used as a fallback/debug artifact.
+
## Notes
- Keep the adapter on the host OS, not inside a VM such as Parallels.
diff --git a/apps/desktop/electrobun.config.ts b/apps/desktop/electrobun.config.ts
index 03b9319..a5b74a9 100644
--- a/apps/desktop/electrobun.config.ts
+++ b/apps/desktop/electrobun.config.ts
@@ -1,10 +1,16 @@
+import { readFileSync } from "node:fs";
import type { ElectrobunConfig } from "electrobun";
+const cliPackage = JSON.parse(
+ readFileSync(new URL("../cli/package.json", import.meta.url), "utf8"),
+) as { version?: string };
+const appVersion = cliPackage.version ?? "0.0.1";
+
export default {
app: {
name: "Axon Servo Programmer",
identifier: "com.caryden.axon.servo-programmer",
- version: "0.0.1",
+ version: appVersion,
},
build: {
views: {
@@ -29,4 +35,7 @@ export default {
bundleCEF: false,
},
},
+ scripts: {
+ postWrap: "./scripts/post-wrap.ts",
+ },
} satisfies ElectrobunConfig;
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 18771cf..ac2c2cf 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,10 +1,15 @@
{
"name": "@axon/desktop-poc",
- "version": "0.0.1",
+ "version": "1.1.0",
"private": true,
"type": "module",
"scripts": {
- "check": "bun x @biomejs/biome check src package.json electrobun.config.ts",
+ "check": "bun x @biomejs/biome check src scripts package.json electrobun.config.ts",
+ "clean": "rm -rf build artifacts",
+ "build": "electrobun build --env=dev",
+ "build:canary": "electrobun build --env=canary",
+ "build:stable": "electrobun build --env=stable",
+ "package": "electrobun build --env=stable",
"start": "electrobun dev",
"dev": "electrobun dev --watch",
"typecheck": "tsc --noEmit"
diff --git a/apps/desktop/scripts/post-wrap.ts b/apps/desktop/scripts/post-wrap.ts
new file mode 100644
index 0000000..88d179c
--- /dev/null
+++ b/apps/desktop/scripts/post-wrap.ts
@@ -0,0 +1,35 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+
+const wrapperBundlePath = process.env.ELECTROBUN_WRAPPER_BUNDLE_PATH;
+const appVersion = process.env.ELECTROBUN_APP_VERSION;
+const targetOs = process.env.ELECTROBUN_OS;
+
+if (!wrapperBundlePath || !appVersion || targetOs !== "macos") {
+ process.exit(0);
+}
+
+const infoPlistPath = join(wrapperBundlePath, "Contents", "Info.plist");
+const original = readFileSync(infoPlistPath, "utf8");
+
+if (original.includes("CFBundleShortVersionString")) {
+ process.exit(0);
+}
+
+const replacement = [
+ "CFBundleVersion",
+ ` ${appVersion}`,
+ " CFBundleShortVersionString",
+ ` ${appVersion}`,
+].join("\n");
+
+const updated = original.replace(
+ /CFBundleVersion<\/key>\s*.*?<\/string>/,
+ replacement,
+);
+
+if (updated === original) {
+ throw new Error(`Could not patch ${infoPlistPath} with CFBundleShortVersionString`);
+}
+
+writeFileSync(infoPlistPath, updated);
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index 173ef14..66141b2 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -13,6 +13,7 @@ Pick the install method that matches how you work.
| macOS (Apple Silicon or Intel)| `curl -fsSL https://github.com/caryden/servo-programmer/releases/latest/download/install.sh \| bash` |
| Linux (x64 or arm64) | `curl -fsSL https://github.com/caryden/servo-programmer/releases/latest/download/install.sh \| bash` |
| Windows (x64) | Download `axon-windows-x64.exe`, rename it to `axon.exe`, and put it on `%PATH%` |
+| macOS desktop app (experimental) | Download `Axon-Servo-Programmer-macos-arm64.dmg` from the latest release and drag the app to `Applications` |
| "I already have a Node toolchain" | `npm install -g @caryden/axon` *(once published)* |
| "I already have a Bun toolchain" | `bunx @caryden/axon` *(once published; runs without installing)* |
| "I'm in CI and need a one-liner" | `curl -fsSL https://github.com/caryden/servo-programmer/releases/latest/download/install.sh \| bash` |
@@ -84,6 +85,30 @@ Pick the install method that matches how you work.
axon --version
```
+## Experimental desktop app (macOS)
+
+The desktop app is an Electrobun side experiment. It shares the same
+core logic and UI as the CLI/web surfaces, but the packaged app itself
+is still experimental.
+
+Download from the [latest release](https://github.com/caryden/servo-programmer/releases/latest):
+
+- `Axon-Servo-Programmer-macos-arm64.dmg`
+
+Install:
+
+1. open the DMG
+2. drag `Axon Servo Programmer.app` into `Applications`
+3. launch it from `Applications`
+
+Notes:
+
+- current packaged desktop builds are macOS Apple Silicon only
+- because the app is not yet signed/notarized, macOS may require
+ `right-click -> Open` on first launch
+- desktop artifacts are published by the tagged release workflow; a
+ merge to `main` does not by itself create a downloadable DMG
+
## Homebrew (macOS / Linuxbrew)
Tap the formula and install: