---
name: AgentDeck Deploy
slug: agentdeck-deploy
category: DevOps
description: AgentDeck Deploy builds, installs, launches, and configures AgentDeck on connected Android, Apple, ESP32, Stream Deck, and daemon targets. Use it to deploy to specific devices or run a full multi-target release.
github: "https://github.com/puritysb/AgentDeck/tree/master/.agents/skills/agentdeck-deploy"
language: TypeScript
stars: 219
forks: 42
install: "npx degit https://github.com/puritysb/AgentDeck/tree/master/.agents/skills/agentdeck-deploy ~/.claude/skills/agentdeck-deploy"
installs_to: ~/.claude/skills/agentdeck-deploy
source_path: .agents/skills/agentdeck-deploy/SKILL.md
collection_size: 5
category_size: 920
collection_url: "https://dirskills.com/collections/puritysb/AgentDeck"
added: 2026-09-04T05:25:07.646Z
last_synced: 2026-09-04T05:25:07.646Z
canonical_url: "https://dirskills.com/skills/agentdeck-deploy"
---

# AgentDeck Deploy

AgentDeck Deploy builds, installs, launches, and configures AgentDeck on connected Android, Apple, ESP32, Stream Deck, and daemon targets. Use it to deploy to specific devices or run a full multi-target release.

**Install:**

```bash
npx degit https://github.com/puritysb/AgentDeck/tree/master/.agents/skills/agentdeck-deploy ~/.claude/skills/agentdeck-deploy
```

## README

# AgentDeck Deploy

Canonical deploy procedure for AgentDeck. This file is the single source of truth — `.claude/skills/deploy.md` is a thin pointer to it. Build, install, launch, and configure AgentDeck on connected targets; keep deployment scoped to what is actually connected and skip missing devices with a clear note (never fail the whole deploy because one device is absent).

## Arguments / Target Names

Parse the argument string to determine target(s). Multiple targets can be combined (e.g. `android ios`).

| Argument | Scope |
|----------|-------|
| (none) / `all` | Full deploy: bridge → plugin → android → ios → macos |
| `android` | All connected Android devices |
| `pantone` / `pantone6` | Pantone 6 only |
| `crema` | Crema S only |
| `lenovo` / `tablet` / `tab` | Lenovo Tab only |
| `ios` | All iOS devices (iPad + iPhone) |
| `iphone` | iPhone XR only |
| `ipad` | iPad Air M2 only |
| `macos` / `mac` | macOS app only |
| `apple` | iOS + macOS |
| `esp32` | All connected ESP32 display boards (excludes Ulanzi TC001) |
| `esp32-all` | All ESP32 boards including Ulanzi TC001 |
| `inkdeck` | InkDeck e-ink only |
| `nm_epd_420` / `nm` | RockBase NM-EPD-420 only |
| `lilygo_epd47` / `epd47` | LilyGo T5 ePaper S3 only |
| `ulanzi` / `tc001` | Ulanzi TC001 LED matrix only |
| `bridge` / `daemon` | Daemon restart only |
| `plugin` / `sd` | Stream Deck plugin only |

## Device Registry

### Android Devices

| Device | Serial | Type | Quirks |
|--------|--------|------|--------|
| **Pantone 6** | `AA007422R24C1300039` | Color e-ink (Kaleido 3, RK3566) | Rotation reset on reinstall → must restore landscape. WRITE_SETTINGS permission lost on reinstall |
| **Crema S** | `CREMAA21W09235` | B&W e-ink (RK3566) | Standard |
| **Lenovo Tab** | `HVA095B4` | LCD tablet (J606F) | Standard |

### Apple Devices

| Device | devicectl ID | xcodebuild destination | Type |
|--------|-------------|----------------------|------|
| **iPad Air 11" (M2)** | `8B71247D-A740-535E-8B2C-6FE9A196F342` | `platform=iOS,id=00008112-001608A02ED2601E` | WiFi/USB |
| **iPhone XR** | `E5F3252C-69A4-5AC9-9E9A-BC2B328D24E3` | `platform=iOS,id=E5F3252C-69A4-5AC9-9E9A-BC2B328D24E3` | WiFi/USB |
| **macOS** | — | `platform=macOS` | Local |

### ESP32 Boards

> **두 이름 층** (panel/form + 인치): `friendly` 는 `./scripts/flash.sh <friendly>` 에 넘기는 canonical 친근명, `pio env` 는 `pio run -e <pio env>` / `.pio/build/<pio env>/` 에 쓰는 실제 PlatformIO env. flash.sh 는 둘 다 받지만 `pio run -e` 는 **pio env 만** 받는다.

| Board | friendly (flash.sh) | pio env (pio run -e) | Serial Pattern | Chip |
|-------|------------|------------|----------------|------|
| **86 Box** (480×480) | `box_40` | `box_86` | `/dev/cu.usbserial-*` | ESP32-S3, CH340 |
| **IPS 3.5"** (480×320) | `ips_35` | `ips35` | `/dev/cu.usbmodem*` | ESP32-S3, native USB |
| **Round AMOLED** (360×360) | `amoled_18` | `amoled` | `/dev/cu.usbmodem*` | ESP32-S3, native USB |
| **TTGO T-Display** (135×240) | `tft_114` | `ttgo` | `/dev/cu.wchusbserial*` | ESP32-D0WDQ6, CH340 |
| **IPS 10.1"** (800×1280) | `ips_101` | `ips10` | `/dev/cu.wchusbserial*` | ESP32-P4 + C6 |
| **InkDeck** (800×480 e-ink) | `inkdeck` | `inkdeck` | `/dev/cu.usbmodem*` | XIAO ESP32-S3 Plus |
| **RockBase NM-EPD-420** (400×300 e-ink) | `nm_epd_420` | `nm_epd_420` | `/dev/cu.usbmodem*` | ESP32-S3 N16R8, native USB |
| **LilyGo T5 ePaper S3** (960×540 e-ink) | `lilygo_epd47` | `lilygo_epd47` | `/dev/cu.usbmodem*` | ESP32-S3 N16R8, native USB |
| **T-Embed CC1101** (170×320 + encoder) | `t_embed` | `t_embed` | `/dev/cu.usbmodem*` | ESP32-S3, native USB |
| **T-Display-S3-Pro** (222×480) | `t_display_pro` | `t_display_pro` | `/dev/cu.usbmodem*` | ESP32-S3, native USB |
| **Ulanzi TC001** (8×32 LED) | `led_8x32` | `led8x32` | `/dev/cu.usbserial-*` | ESP32-D0WD classic, CH340 |

## Execution Steps

### Step 0: Pre-flight — Detect Connected Devices

Run before any deploy to know what's available:

```bash
cd /Users/puritysb/github/AgentDeck

echo "=== ADB Devices ==="
adb devices -l 2>/dev/null | grep -w device | grep -v "List"

echo "=== Apple Devices ==="
xcrun devicectl list devices 2>/dev/null | grep -E "(iPhone|iPad)" || echo "none"

echo "=== Serial Ports (ESP32) ==="
ls /dev/cu.usb* 2>/dev/null || echo "none"

echo "=== Stream Deck ==="
pgrep -x "Stream Deck" >/dev/null && echo "running" || echo "not running"

echo "=== Daemon ==="
cat ~/.agentdeck/daemon.json 2>/dev/null || echo "not running"
```

Only deploy to devices that are actually connected. Skip missing devices with a warning, don't fail. If any preflight command fails because of sandboxing or device access, request scoped approval and retry only that command — do not guess.

### Step 1: Build (always first, unless target is bridge-only or esp32-only)

```bash
cd /Users/puritysb/github/AgentDeck
pnpm build
```

For Android targets, also build APK:
```bash
bash scripts/build-android-release.sh
```
This produces `dist/agentdeck-v{VERSION}.apk`.

For Apple targets (ios/macos), build via xcodebuild (see Step 3/4).

### Step 2: Android Deploy

For EACH connected Android device in the target set:

```bash
SERIAL="<device_serial>"
APK="dist/agentdeck-v*.apk"  # use the actual versioned filename from Step 1

# 1. Stop running app
adb -s $SERIAL shell am force-stop dev.agentdeck

# 2. Install (attempt direct first, uninstall on signature conflict)
if ! adb -s $SERIAL install -r $APK 2>&1 | grep -q "Success"; then
  adb -s $SERIAL uninstall dev.agentdeck
  adb -s $SERIAL install $APK
fi

# 3. Launch
adb -s $SERIAL shell am start -n dev.agentdeck/.MainActivity

# 4. adb reverse for daemon connection
adb -s $SERIAL reverse tcp:9120 tcp:9120
```

**Device-specific post-install:**

**Pantone 6** (after ANY install, especially after uninstall→reinstall):
```bash
# Rotation fix — reinstall resets system rotation settings (NOTE: Pantone 6 only! Do NOT run on Crema S or ordinary tablets)
adb -s AA007422R24C1300039 shell settings put system accelerometer_rotation 0
adb -s AA007422R24C1300039 shell settings put system user_rotation 1  # 1=landscape
```

**Crema S**: No special steps needed. (Standard `requestedOrientation` is used; do not modify system-wide `user_rotation`.)

**Lenovo Tab**: No special steps needed.

### Step 3: iOS Deploy

Build once, install on multiple devices:

```bash
cd /Users/puritysb/github/AgentDeck/apple

# Build (one build serves both devices)
xcodebuild build -project AgentDeck.xcodeproj -scheme AgentDeck_iOS \
  -destination 'platform=iOS,id=00008112-001608A02ED2601E' \
  CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=QF36NDHYHD -quiet

APP=~/Library/Developer/Xcode/DerivedData/AgentDeck-dqyrhbwpqboxgiabhllzxkkjxqzy/Build/Products/Debug-iphoneos/AgentDeck.app
```

For EACH iOS device in the target set:
```bash
DEVICE_ID="<devicectl_id>"
xcrun devicectl device install app --device $DEVICE_ID $APP
xcrun devicectl device process launch --device $DEVICE_ID bound.serendipity.agent.deck
```

**Important:**
- Devices must be unlocked (passcode protected → install fails)
- After uninstall→reinstall, local network permission popup appears again
- Prefer existing XcodeBuildMCP tools when available; otherwise use these established `xcodebuild` commands

### Step 4: macOS Deploy

```bash
cd /Users/puritysb/github/AgentDeck/apple
xcodebuild build -project AgentDeck.xcodeproj -scheme AgentDeck_macOS \
  -destination 'platform=macOS' -quiet

# Kill existing → relaunch
killall AgentDeck 2>/dev/null; sleep 0.5
open -a "/Users/puritysb/Library/Developer/Xcode/DerivedData/AgentDeck-dqyrhbwpqboxgiabhllzxkkjxqzy/Build/Products/Debug/AgentDeck.app"
```

Do not add or alter App Store UI text that asks users to install or launch external tools (App Review 4.2.3 — see `CLAUDE.md` "App Store build invariants").

### Step 5: Bridge/Daemon Restart

```bash
agentdeck daemon stop 2>/dev/null
sleep 1
agentdeck daemon start &
sleep 2
agentdeck daemon status
```

### Step 6: Plugin

If Stream Deck is running and plugin is linked (`streamdeck link`), `pnpm build` (Step 1) is sufficient.

For fresh install:
```bash
cd /Users/puritysb/github/AgentDeck
pnpm package
# Output: dist/bound.serendipity.agentdeck.streamDeckPlugin
# User must drag into Stream Deck app manually
```

### Step 7: ESP32 Firmware

**CRITICAL: Build and flash ONE AT A TIME** — PlatformIO lock + serial port conflicts.

**Ulanzi TC001 is a separate target.** The `esp32` target deploys display boards only (86 Box, IPS 3.5", Round AMOLED, T-Embed CC1101, T-Display-S3-Pro, InkDeck, NM-EPD-420, and LilyGo EPD47). Use `ulanzi`, `tc001`, or `esp32-all` to include the Ulanzi TC001. This separation exists because:
- Ulanzi uses a different chip (ESP32-D0WD classic vs ESP32-S3)
- Ulanzi uses FastLED matrix rendering, not LVGL — UI changes to cloud.cpp/theme.h don't affect it
- Ulanzi requires a different flash procedure (esptool full-flash vs PIO upload)

**CRITICAL: Identify boards by `device_info` BEFORE flashing.** Port numbers change when USB hub positions change — never assume a port number means a specific board.

```bash
cd /Users/puritysb/github/AgentDeck/esp32

# Step 1: Detect and IDENTIFY each board
for port in /dev/cu.usb*; do
  echo "=== $port ==="
  # Send device_info_request, read 1 line with timeout
  (echo '{"type":"device_info_request"}' > "$port" &) 2>/dev/null
  timeout 2 head -1 < "$port" 2>/dev/null | grep -o '"board":"[^"]*"' || echo "no response"
done
# Match each port to its board name before flashing!
```

If the daemon is holding a serial port, stop the daemon (or use the flash helper that pauses/resumes it) before flashing — see `docs/esp32.md`.

#### Display Boards (86 Box, IPS 3.5", Round AMOLED, T-Embed CC1101, T-Display-S3-Pro)

Flash each detected display board:
```bash
# Match port to environment and flash
pio run -e <environment> -t upload --upload-port <port>
```

**86 Box CH340 fallback**: If PIO upload fails at high baud (chip stops responding), build separately then flash with esptool at 115200:
```bash
pio run -e box_86  # build only
~/.platformio/penv/bin/esptool --chip esp32s3 --port <port> --baud 115200 \
  --before default-reset --after hard-reset write-flash -z \
  --flash-mode dio --flash-freq 80m --flash-size 8MB \
  0x0 .pio/build/box_86/bootloader.bin \
  0x8000 .pio/build/box_86/partitions.bin \
  0x10000 .pio/build/box_86/firmware.bin
```

After flash: USB re-plug required for JTAG boards (IPS 3.5", Round AMOLED).

#### Native e-ink boards

All three must be identified by `device_info` before flashing because native USB
port numbers change. InkDeck additionally re-enumerates to a distinct download
node; choose that new node after reset. NM and LilyGo use the confirmed 115200
upload speed.

```bash
./scripts/flash.sh inkdeck <device_info-confirmed-port>
./scripts/flash.sh nm_epd_420 <device_info-confirmed-port>
./scripts/flash.sh lilygo_epd47 <device_info-confirmed-port>
```

#### Ulanzi TC001 (separate target)

**Only flash when `ulanzi`, `tc001`, or `esp32-all` is specified.** Skip for plain `esp32` target.

Use the helper script or the equivalent `esptool` command at `115200`:
```bash
cd /Users/puritysb/github/AgentDeck/esp32
./scripts/flash.sh led_8x32 /dev/cu.usbserial-211110
```

Equivalent manual fallback:
```bash
cd /Users/puritysb/github/AgentDeck/esp32
~/.platformio/penv/bin/esptool --chip esp32 --port /dev/cu.usbserial-211110 --baud 115200 \
  --before default-reset --after hard-reset write-flash -z \
  --flash-mode dio --flash-freq 40m --flash-size 8MB \
  0x1000 .pio/build/led8x32/bootloader.bin \
  0x8000 .pio/build/led8x32/partitions.bin \
  0x10000 .pio/build/led8x32/firmware.bin
```

Notes:
- `460800` is not reliable on the TC001's CH340 path
- always flash `bootloader + partitions + firmware` together
- if the daemon is holding the serial port, the helper script pauses it and resumes it after flash

## Verification

After all deploys complete, verify each target:

```bash
# Android: check process running
for d in AA007422R24C1300039 CREMAA21W09235 HVA095B4; do
  adb -s $d shell pidof dev.agentdeck 2>/dev/null && echo "$d: running" || echo "$d: not running"
done

# Daemon
curl -s http://localhost:9120/health | head -1

# Plugin / Devices
agentdeck devices 2>/dev/null
```

## Output

Print a summary table at the end. Example:

```
Deploy Summary
══════════════════════════════════════════════════
 Target          Status   Details
──────────────────────────────────────────────────
 Bridge           OK      Daemon port 9120
 Plugin           OK      Built, SD running
 Pantone 6        OK      Installed + launched + rotation fix
 Crema S          OK      Installed + launched
 Lenovo Tab       OK      Installed + launched
 iPad Air         OK      Installed + launched
 iPhone           SKIP    Not connected
 macOS            OK      Built + launched
 ESP32            SKIP    No firmware changes
══════════════════════════════════════════════════
```

Use SKIP (not connected / not in target), OK (success), FAIL (error with reason). Always report which commands required approval.

## Error Handling

- **Signature mismatch**: Auto-uninstall → reinstall. Warn user that app data is lost
- **Device not connected**: SKIP with warning, don't fail the whole deploy
- **Build failure**: Stop immediately, show error. Don't install stale APK
- **iOS device locked**: Warn "device must be unlocked" and skip
- **ESP32 upload hang**: Kill after 60s timeout, suggest esptool fallback
