Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions .github/workflows/finalize-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
MARKER="$HOME/Applications/.Microbridge.app.microbridge-brew"
LEGACY_MARKER="$APP/.microbridge-brew"
test -x "$(brew --prefix DevVig/microbridge/microbridge)/bin/microbridged"
brew services start DevVig/microbridge/microbridge
microbridge-app install
for _ in {1..30}; do
[[ -f "$MARKER" ]] && break
sleep 1
Expand All @@ -72,6 +72,33 @@ jobs:
spctl --assess --type execute --verbose=4 "$APP"
xcrun stapler validate "$APP"
syspolicy_check distribution "$APP"
APP_EXECUTABLE="$APP/Contents/MacOS/microbridge-ui"
APP_LOG="$RUNNER_TEMP/microbridge-app.log"
APP_PID=""
for _ in {1..30}; do
APP_PID="$(pgrep -f "^${APP_EXECUTABLE}$" | head -n1 || true)"
[[ -n "$APP_PID" ]] && break
sleep 1
done
Comment on lines +78 to +82
if [[ -z "$APP_PID" ]] || ! kill -0 "$APP_PID"; then
cat "$APP_LOG"
exit 1
fi
microbridgectl status >"$APP_LOG"
kill "$APP_PID" || true
wait "$APP_PID" || true
APP_DAEMON_STOPPED=0
for _ in {1..30}; do
if ! microbridgectl status >/dev/null 2>&1; then
APP_DAEMON_STOPPED=1
break
fi
sleep 1
done
test "$APP_DAEMON_STOPPED" -eq 1
# Verify the separately opted-in headless service after the normal
# app-owned lifecycle.
brew services start DevVig/microbridge/microbridge
SERVICE_STATE=""
for _ in {1..30}; do
SERVICE_STATE="$(brew services list --json | jq -r 'map(select(.name=="microbridge")) | .[0].status // empty')"
Expand All @@ -83,25 +110,13 @@ jobs:
tail -200 "$(brew --prefix)/var/log/microbridge.log" || true
exit 1
fi
APP_LOG="$RUNNER_TEMP/microbridge-app.log"
"$APP/Contents/MacOS/microbridge-ui" >"$APP_LOG" 2>&1 &
APP_PID=$!
sleep 3
if ! kill -0 "$APP_PID"; then
cat "$APP_LOG"
exit 1
fi
kill "$APP_PID" || true
wait "$APP_PID" || true
brew services stop DevVig/microbridge/microbridge
microbridge-app uninstall
HOMEBREW_NO_INSTALL_CLEANUP=1 brew uninstall DevVig/microbridge/microbridge
if brew services list --json | jq -e '.[] | select(.name=="microbridge")' >/dev/null; then
echo "microbridge service is still registered after uninstall" >&2
exit 1
fi
test -f "$MARKER"
rm -rf "$APP"
rm -f "$MARKER"
test ! -e "$APP"
test ! -e "$MARKER"

Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,12 @@ jobs:
```sh
brew tap DevVig/microbridge https://github.com/DevVig/microbridge
brew install microbridge
brew services start microbridge
open ~/Applications/Microbridge.app
microbridge-app install
```

Upgrade later: `brew update && brew upgrade microbridge`
Upgrade later: `brew update && brew upgrade microbridge && microbridge-app install`

Headless-only daemon service: `brew services start microbridge`

### Signed + notarized DMG

Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = [
]

[workspace.package]
version = "0.3.7"
version = "0.3.8"
edition = "2021"
license = "MIT"
repository = "https://github.com/DevVig/microbridge"
Expand Down
70 changes: 54 additions & 16 deletions Formula/microbridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
#
# brew tap DevVig/microbridge https://github.com/DevVig/microbridge
# brew install microbridge
# brew services start microbridge
# open ~/Applications/Microbridge.app
# microbridge-app install
#
# Upgrade:
# brew update && brew upgrade microbridge
Expand Down Expand Up @@ -58,37 +57,72 @@ def install
# INSTALL.md ships inside the daemon archive when present.
doc.install "INSTALL.md" if File.exist?("INSTALL.md")

# Homebrew sandboxes formula post_install and forbids writes to $HOME.
# The launch-agent runs in the user's session, so this wrapper performs the
# marker-guarded app copy immediately before starting the daemon.
service_script = libexec/"microbridge-service"
service_script.write <<~SH
# Homebrew sandboxes formula installation from $HOME. This explicit helper
# performs the marker-guarded GUI install without registering a daemon
# service; `brew services` remains available for deliberate headless use.
app_installer = bin/"microbridge-app"
app_installer.write <<~SH
#!/bin/sh
set -eu
source_app="#{opt_prefix}/Microbridge.app"
apps_dir="${HOME}/Applications"
dest="${apps_dir}/Microbridge.app"
marker="${apps_dir}/.Microbridge.app.microbridge-brew"
legacy_marker="${dest}/.microbridge-brew"
stop_managed_app() {
executable="${dest}/Contents/MacOS/microbridge-ui"
/usr/bin/pgrep -f "^${executable}$" 2>/dev/null | while read -r pid; do
/bin/kill "${pid}" 2>/dev/null || true
done
for _ in 1 2 3 4 5 6 7 8 9 10; do
/usr/bin/pgrep -f "^${executable}$" >/dev/null 2>&1 || return 0
/bin/sleep 0.1
done
}
Comment on lines +72 to +81
action="${1:-install}"
if [ "${action}" = "uninstall" ]; then
if [ -f "${marker}" ] || [ -f "${legacy_marker}" ]; then
if [ -x "${dest}/Contents/MacOS/microbridge-ui" ]; then
"${dest}/Contents/MacOS/microbridge-ui" --unregister-login-item || true
fi
stop_managed_app
/bin/rm -rf "${dest}"
/bin/rm -f "${marker}"
else
echo "Microbridge: preserving unowned ${dest}" >&2
fi
exit 0
fi
if [ "${action}" != "install" ]; then
echo "usage: microbridge-app [install|uninstall]" >&2
exit 2
fi
/bin/mkdir -p "${apps_dir}"
if [ -e "${dest}" ] && [ ! -f "${marker}" ] && [ ! -f "${legacy_marker}" ]; then
echo "Microbridge: preserving unowned ${dest}" >&2
exit 1
else
staging="${apps_dir}/.Microbridge.app.installing.$$"
trap '/bin/rm -rf "${staging}"' EXIT
/bin/rm -rf "${staging}"
/usr/bin/ditto "${source_app}" "${staging}"
/usr/bin/codesign --verify --deep --strict "${staging}"
if [ -e "${dest}" ]; then
stop_managed_app
/bin/rm -rf "${dest}"
fi
/usr/bin/ditto "${source_app}" "${dest}"
/bin/mv "${staging}" "${dest}"
# Keep ownership state beside the signed bundle. Adding any file to
# Microbridge.app invalidates its sealed code signature.
/usr/bin/touch "${marker}"
/usr/bin/open "${dest}"
fi
exec "#{opt_bin}/microbridged"
SH
service_script.chmod 0755
app_installer.chmod 0755
end

service do
run [opt_libexec/"microbridge-service"]
run [opt_bin/"microbridged"]
keep_alive true
log_path var/"log/microbridge.log"
error_log_path var/"log/microbridge.log"
Expand All @@ -100,24 +134,28 @@ def caveats
Microbridge is the menu bar app + a local daemon (not CLI-only).

App: ~/Applications/Microbridge.app
Daemon: brew services start microbridge
Daemon: app-owned (standard) or brew services (headless)
Status: microbridgectl status
Config: ~/.microbridge/

Start the service once to install the marker-owned app, then open it. The
app will offer to start itself at login (change it in Settings > General):
Install or refresh the marker-owned app, then let it own the bundled daemon:
microbridge-app install

The app will offer to start itself at login (change it in Settings > General).

Optional headless daemon service (this creates a separate background item):
brew services start microbridge
open ~/Applications/Microbridge.app

Hardware LEDs/keys need a connected Codex Micro and explicit consent in
Microbridge Settings → Device → Enable hardware control.

Upgrade: brew update && brew upgrade microbridge
Upgrade: brew update && brew upgrade microbridge && microbridge-app install
EOS
end

test do
assert_match "Usage", shell_output("#{bin}/microbridgectl help")
assert_path_exists prefix/"Microbridge.app"
assert_path_exists bin/"microbridge-app"
end
end
54 changes: 30 additions & 24 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,21 @@ runs on your machine.
## Recommended on macOS: Homebrew (with updates)

This is the easy path. You do **not** need to clone the repo. Homebrew installs
the **menu bar app** (primary UI) and the daemon, then owns upgrades and the
daemon service. This is not a CLI-only product.
the **menu bar app** (primary UI), its bundled daemon, and the CLI. The explicit
app helper preserves the signed bundle and avoids a separate background item.

```sh
brew tap DevVig/microbridge https://github.com/DevVig/microbridge
brew install microbridge
brew services start microbridge
open ~/Applications/Microbridge.app
microbridge-app install
microbridgectl status
```

### Updates

```sh
brew update && brew upgrade microbridge
brew services restart microbridge
microbridge-app install
```

Optional **background** upgrades (Homebrew’s autoupdate):
Expand All @@ -39,11 +38,16 @@ tarball.
Uninstall:

```sh
brew services stop microbridge
microbridge-app uninstall
brew uninstall microbridge
# optional: brew untap DevVig/microbridge
```

Advanced headless mode: `brew services start microbridge` runs the standalone
daemon without the menu-bar app and intentionally creates a separate background
item. Stop it with `brew services stop microbridge` before returning to the
standard app-owned lifecycle.

Governance / why this path: [docs/governance.md](docs/governance.md).

---
Expand All @@ -54,14 +58,14 @@ Governance / why this path: [docs/governance.md](docs/governance.md).
|---|---|
| macOS (Homebrew) | Homebrew + **Xcode Command Line Tools** (`xcode-select --install`); Rust + Node pulled in as **build** deps (builds `.app` + daemon) |
| From source | Rust stable, Node ≥ 20; macOS also needs Xcode CLT for the `.app` |
| Hardware LEDs/keys | Codex Micro over USB; enable **Settings → Device → Hardware control** (`MICROBRIDGE_HID_CLAIM=1` remains a developer override) |
| Hardware LEDs/keys | Codex Micro over USB; claim it from the popover, the menu-bar icon’s right-click menu, or **Settings → Device** (`MICROBRIDGE_HID_CLAIM=1` remains a developer override) |

## From source (developers)

```sh
git clone https://github.com/DevVig/microbridge.git
cd microbridge
./scripts/install.sh # macOS: daemon + menu bar app + launchd
./scripts/install.sh # macOS: menu bar app + app-owned daemon
# ./scripts/install.sh --no-ui # daemon/CLI only (headless)
# ./scripts/install-linux-systemd.sh
```
Expand Down Expand Up @@ -105,9 +109,10 @@ app-originated network call. The daemon also contacts a T3 Code environment
only after you explicitly enable that integration and exchange a one-time pairing
link; Microbridge has no telemetry or cloud relay.

Homebrew installs are managed by brew instead: the app detects the brew
marker and points you at `brew upgrade microbridge` rather than self-replacing,
so the formula version and the on-disk app never drift apart.
Homebrew installs are managed by brew instead: the app detects the brew marker
and points you at `brew update && brew upgrade microbridge && microbridge-app
install` rather than self-replacing, so the formula version and the stable app
copy never drift apart.

### Cursor integration

Expand Down Expand Up @@ -166,35 +171,36 @@ Tauri build). The formula checksums are refreshed by CI after each `v*` tag.
| `~/.local/bin/microbridged` | Daemon (source / release install) |
| `~/.microbridge/microbridged.sock` | Local NDJSON socket |
| `~/.microbridge/config.toml` | Key source, lighting, appearance |
| `~/.microbridge/daemon.log` | launchd / service logs |
| `~/.microbridge/microbridged-app.log` | Standard app-owned daemon log |
| `~/.microbridge/daemon.log` | Headless launchd / service log |
| `~/.cursor/plugins/local/microbridge` | Bundled Cursor lifecycle integration (only after consent) |
| `~/.factory/hooks.json` | Existing Factory hooks plus Microbridge-owned lifecycle entries (only after consent) |
| `~/.microbridge/integrations/factory/microbridgectl` | Signed Factory hook helper (only after consent) |
| `~/.config/opencode/plugins/microbridge.mjs` | Bundled OpenCode lifecycle and interrupt integration (only after consent) |
| `~/Library/LaunchAgents/ai.microbridge.ui.plist` | Login item (only if you enable launch at login) |
| macOS Login Items | Branded Microbridge main-app registration (only if enabled in Settings → General) |

## Launch at login

The menu bar app asks once, on first launch, whether to start automatically at
login, and writes the `ai.microbridge.ui` LaunchAgent if you say yes. Toggle it
any time in **Settings → General**; it takes effect at your next login. This is
handled by the app rather than the installer, so Homebrew, DMG, and source
installs all behave the same way.
login, and registers the signed main app with macOS ServiceManagement if you say
yes. Toggle it any time in **Settings → General**; if macOS requires approval,
the same surface opens Login Items directly. The standard GUI path shows the
Microbridge name and icon rather than a Unix executable.

## Troubleshooting

**`microbridgectl: connect …`** — daemon not running. Direct installs start the
bundled daemon with the app; relaunch Microbridge first. For Homebrew installs:
**`microbridgectl: connect …`** — daemon not running. Standard GUI installs
start the bundled daemon with the app; relaunch Microbridge first. For explicit
headless operation:

```sh
brew services restart microbridge
# or:
launchctl kickstart -k "gui/$(id -u)/ai.microbridge.daemon"
```

**LEDs stay dark** — by default Microbridge only probes USB (Detected). Enable
**Settings → Device → Hardware control**. If the interface is busy, pause the
other device owner and try again. Developers can still set
**LEDs stay dark** — by default Microbridge only probes USB (Detected). Choose
**Claim Codex Micro** in the popover or right-click menu. If the interface is
busy, pause the other device owner and choose **Retry**. The advanced control
also remains in **Settings → Device**. Developers can still set
`MICROBRIDGE_HID_CLAIM=1` before starting the daemon. See
[docs/device-hid.md](docs/device-hid.md).

Expand Down
Loading
Loading