diff --git a/scripts/build-boss-agent-mac-app.sh b/scripts/build-boss-agent-mac-app.sh
index 2e0e63b..e4d049e 100644
--- a/scripts/build-boss-agent-mac-app.sh
+++ b/scripts/build-boss-agent-mac-app.sh
@@ -8,12 +8,19 @@ MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
SOURCE_FILE="$ROOT_DIR/apps/boss-agent-mac/Sources/BossAgentApp.swift"
BINARY_PATH="$MACOS_DIR/boss-agent"
+ICONSET_DIR="$RESOURCES_DIR/BossAgent.iconset"
+ICON_PATH="$RESOURCES_DIR/BossAgent.icns"
if ! command -v swiftc >/dev/null 2>&1; then
echo "swiftc not found. Install Xcode Command Line Tools first." >&2
exit 1
fi
+if ! command -v iconutil >/dev/null 2>&1; then
+ echo "iconutil not found. Install Xcode Command Line Tools first." >&2
+ exit 1
+fi
+
rm -rf "$APP_DIR"
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
@@ -28,6 +35,84 @@ swiftc "$SOURCE_FILE" \
chmod +x "$BINARY_PATH"
+python3 - "$ICONSET_DIR" <<'PY'
+import os
+import struct
+import sys
+import zlib
+
+iconset_dir = sys.argv[1]
+os.makedirs(iconset_dir, exist_ok=True)
+
+targets = {
+ "icon_16x16.png": 16,
+ "icon_16x16@2x.png": 32,
+ "icon_32x32.png": 32,
+ "icon_32x32@2x.png": 64,
+ "icon_128x128.png": 128,
+ "icon_128x128@2x.png": 256,
+ "icon_256x256.png": 256,
+ "icon_256x256@2x.png": 512,
+ "icon_512x512.png": 512,
+ "icon_512x512@2x.png": 1024,
+}
+
+def rounded_rect(px, py, x0, y0, x1, y1, radius):
+ cx = min(max(px, x0 + radius), x1 - radius)
+ cy = min(max(py, y0 + radius), y1 - radius)
+ return (px - cx) * (px - cx) + (py - cy) * (py - cy) <= radius * radius
+
+def rgba_at(px, py):
+ if rounded_rect(px, py, 82, 82, 942, 942, 210):
+ white = (
+ rounded_rect(px, py, 278, 245, 406, 779, 64)
+ or rounded_rect(px, py, 350, 245, 745, 518, 132)
+ or rounded_rect(px, py, 350, 506, 745, 779, 132)
+ )
+ cutout = (
+ rounded_rect(px, py, 462, 332, 619, 438, 54)
+ or rounded_rect(px, py, 462, 592, 635, 698, 54)
+ )
+ if white and not cutout:
+ return (255, 255, 255, 255)
+ return (7, 193, 96, 255)
+
+ if rounded_rect(px, py - 18, 82, 82, 942, 942, 210):
+ return (7, 24, 16, 28)
+ return (0, 0, 0, 0)
+
+def write_png(path, size):
+ rows = []
+ for y in range(size):
+ row = bytearray([0])
+ for x in range(size):
+ px = (x + 0.5) * 1024 / size
+ py = (y + 0.5) * 1024 / size
+ row.extend(rgba_at(px, py))
+ rows.append(bytes(row))
+
+ raw = b"".join(rows)
+ def chunk(kind, data):
+ return (
+ struct.pack(">I", len(data))
+ + kind
+ + data
+ + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
+ )
+
+ with open(path, "wb") as fp:
+ fp.write(b"\x89PNG\r\n\x1a\n")
+ fp.write(chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)))
+ fp.write(chunk(b"IDAT", zlib.compress(raw, 9)))
+ fp.write(chunk(b"IEND", b""))
+
+for name, size in targets.items():
+ write_png(os.path.join(iconset_dir, name), size)
+PY
+
+iconutil -c icns "$ICONSET_DIR" -o "$ICON_PATH"
+rm -rf "$ICONSET_DIR"
+
cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
@@ -45,6 +130,8 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
boss-agent
CFBundleDisplayName
boss-agent
+ CFBundleIconFile
+ BossAgent.icns
CFBundlePackageType
APPL
CFBundleShortVersionString
diff --git a/tests/boss-agent-status.test.mjs b/tests/boss-agent-status.test.mjs
index 1141007..6fb446e 100644
--- a/tests/boss-agent-status.test.mjs
+++ b/tests/boss-agent-status.test.mjs
@@ -219,4 +219,7 @@ test("boss-agent mac app intercepts permission links and triggers native app per
assert.match(buildScript, /NSCameraUsageDescription/);
assert.match(buildScript, /NSAppleEventsUsageDescription/);
assert.match(buildScript, /NSLocalNetworkUsageDescription/);
+ assert.match(buildScript, /CFBundleIconFile/);
+ assert.match(buildScript, /BossAgent\.icns/);
+ assert.match(buildScript, /iconutil -c icns/);
});