Files
Codeman/packages/desktop/scripts/bundle-macos.sh
T
Codeman maintainer 8ba5134174 feat(desktop): native macOS app that runs, installs or connects to Codeman
Adds packages/desktop, a small native app (Rust: tao, wry/WKWebView, muda,
objc2) that opens Codeman in its own window. The universal app is about 5 MB
(3.2 MB as a DMG) and runs on macOS 11.3 or later.

- Connect screen with two tabs. This Mac finds the local Codeman and offers
  Open, Start or Install. Start uses launchd for a LaunchAgent install and
  codeman web -d otherwise. It never starts a second server while one runs,
  because two servers on one data dir share live sessions. Another machine
  connects by address (recent servers, Keychain logins) and can install
  Codeman there over SSH.
- Installs run Codeman's install.sh in Terminal from a self-deleting
  .command script. The installer needs a terminal for sudo, Homebrew and
  ssh. A local install uses --local --service and the first free port from
  3000, and the app opens Codeman once it answers.
- The local Codeman's login is read from its LaunchAgent or
  ~/.codeman/.env. It is sent only to 127.0.0.1 on that port, only while
  the user's own Codeman process holds it, and is never saved.
- Native JS dialogs (alert, confirm, prompt) and HTTP Basic challenge
  answers, both scoped to Codeman's own pages, so the page keeps working
  after a server restart. App cookies are cleared before every connect.
- Pop-outs through window.CodemanHost, with an allowlist (webview/ never
  opens a window), external links in the default browser, downloads to
  ~/Downloads, real full screen and window state that is remembered.
- The connect page refuses a WebKit older than Safari 16.4, which Codeman's
  terminal needs.

Codeman side: viewSubagentTranscript shows a toast when the pop-up is
refused instead of failing, with zh-CN strings and a test that pins them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:36:49 +01:00

111 lines
4.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build Codeman.app (and optionally a .dmg) from packages/desktop, on macOS.
#
# packages/desktop/scripts/bundle-macos.sh # this Mac's architecture
# packages/desktop/scripts/bundle-macos.sh --universal --dmg
# SIGN_IDENTITY="Developer ID Application: ..." packages/desktop/scripts/bundle-macos.sh --dmg
#
# Without SIGN_IDENTITY the app is signed ad hoc: it runs on the Mac that built
# it, and a copy downloaded on another Mac is stopped by Gatekeeper. On macOS 15
# and later, try to open it once, then click Open Anyway in System Settings >
# Privacy & Security; on macOS 14 and earlier, right-click it and choose Open.
# Or run `xattr -dr com.apple.quarantine /Applications/Codeman.app` on the copy
# in /Applications. A Developer ID signature + notarization avoids all of this.
set -euo pipefail
cd "$(dirname "$0")/.."
[ "$(uname -s)" = Darwin ] || { echo "bundle-macos.sh runs on macOS only" >&2; exit 1; }
UNIVERSAL=0
DMG=0
for arg in "$@"; do
case "$arg" in
--universal) UNIVERSAL=1 ;;
--dmg) DMG=1 ;;
*) echo "unknown option: $arg" >&2; exit 2 ;;
esac
done
VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -1)
[ -n "$VERSION" ] || { echo "could not read the version from Cargo.toml" >&2; exit 1; }
# The binary targets the same macOS as LSMinimumSystemVersion in Info.plist,
# so the two cannot disagree.
MACOSX_DEPLOYMENT_TARGET=$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' macos/Info.plist)
[ -n "$MACOSX_DEPLOYMENT_TARGET" ] || { echo "could not read LSMinimumSystemVersion from macos/Info.plist" >&2; exit 1; }
export MACOSX_DEPLOYMENT_TARGET
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# Build with cargo and set BUILT to the executable cargo reports for
# codeman-desktop. Asking cargo, rather than assuming ./target, keeps
# CARGO_TARGET_DIR, build.target-dir and build.target from a cargo config
# working, and a stale binary from an older build is never bundled.
cargo_build() {
local json
json=$(cargo build --locked --release "$@" --message-format=json-render-diagnostics)
BUILT=$(printf '%s\n' "$json" | awk '
index($0, "\"reason\":\"compiler-artifact\"") &&
index($0, "\"kind\":[\"bin\"]") &&
index($0, "\"name\":\"codeman-desktop\"") &&
match($0, /"executable":"[^"]*"/) { path = substr($0, RSTART + 14, RLENGTH - 15) }
END { print path }')
if [ -z "$BUILT" ] || [ ! -x "$BUILT" ]; then
echo "cargo did not report a codeman-desktop executable${BUILT:+ (it named $BUILT)}" >&2
exit 1
fi
}
if [ "$UNIVERSAL" = 1 ]; then
rustup target add aarch64-apple-darwin x86_64-apple-darwin >/dev/null
cargo_build --target aarch64-apple-darwin
ARM_BIN=$BUILT
cargo_build --target x86_64-apple-darwin
INTEL_BIN=$BUILT
BIN="$WORK/codeman-desktop-universal"
lipo -create -output "$BIN" "$ARM_BIN" "$INTEL_BIN"
else
cargo_build
BIN=$BUILT
fi
APP=dist/Codeman.app
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp "$BIN" "$APP/Contents/MacOS/codeman-desktop"
sed "s/__VERSION__/$VERSION/g" macos/Info.plist > "$APP/Contents/Info.plist"
plutil -lint "$APP/Contents/Info.plist" >/dev/null
printf 'APPL????' > "$APP/Contents/PkgInfo"
# assets/icon.png (1024x1024, already on Apple's icon grid) -> Codeman.icns
ICONSET="$WORK/Codeman.iconset"
mkdir -p "$ICONSET"
for size in 16 32 128 256 512; do
sips -z "$size" "$size" assets/icon.png --out "$ICONSET/icon_${size}x${size}.png" >/dev/null
double=$((size * 2))
sips -z "$double" "$double" assets/icon.png --out "$ICONSET/icon_${size}x${size}@2x.png" >/dev/null
done
iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/Codeman.icns"
if [ -n "${SIGN_IDENTITY:-}" ]; then
codesign --force --options runtime --timestamp \
--entitlements macos/entitlements.plist --sign "$SIGN_IDENTITY" "$APP"
else
codesign --force --sign - "$APP"
fi
codesign --verify --strict "$APP"
echo "Built $(pwd)/$APP ($(du -sh "$APP" | cut -f1), version $VERSION, macOS $MACOSX_DEPLOYMENT_TARGET or later)"
if [ "$DMG" = 1 ]; then
STAGE="$WORK/dmg"
mkdir -p "$STAGE"
cp -R "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
DMG_PATH="dist/Codeman-$VERSION.dmg"
rm -f "$DMG_PATH"
hdiutil create -quiet -volname "Codeman" -srcfolder "$STAGE" -ov -format UDZO "$DMG_PATH"
echo "Built $(pwd)/$DMG_PATH ($(du -sh "$DMG_PATH" | cut -f1))"
fi