---
name: macos-rebuild-dev
description: "Kill the running termio dev app, rebuild it with SwiftPM, and relaunch it as a foreground app. Invoke when the user says 'rebuild', 'rebuild app', 'restart app', 'relaunch', '重新编译', or '重启 app'."
---

# Rebuild App (local development)

Kill the running **dev** build, rebuild it as the isolated `termio-dev.app` bundle,
and relaunch — so every rebuild reflects the current Dock icon and, crucially, does
**not** disturb an installed release build you use daily.

## The dev channel

termio ships a `TERMIO_CHANNEL=dev` build that is a fully separate app from the
release one, so both can run at the same time:

| | release | dev (this skill) |
| --- | --- | --- |
| Bundle | `termio.app` (usually `/Applications`) | `termio-dev.app` (repo root) |
| Bundle id | `sh.termio.app` | `sh.termio.app.dev` |
| State + sockets | `~/Library/Application Support/termio` | `…/termio-dev` |
| User config | `~/.termio` | `~/.termio-dev` |
| Daemon socket | `$TMPDIR/termiod-<uid>` | `…-dev` |
| Daemon launchd job | `sh.termio.termiod` | `sh.termio.termiod.dev` |
| Companion port | 8787 | 8788 |
| CLI on PATH | `termio` | `termio-dev` |
| Sparkle auto-update | on | **no update feed** (dev never self-updates) |

All of this falls out of the `.dev` bundle-id suffix via `Sources/termio/Companion/AppChannel.swift`
(paths + port) and `scripts/build-app.sh` (id, name, `SUFeedURL` deletion, CLI rebind).
Sparkle.framework itself is embedded on both channels — the binary links it either
way — so a dev bundle still carries it; it just has nothing to check.

termio is a plain SwiftPM executable (`Package.swift` → `executableTarget` named
`termio`); `swift build` alone produces a bare binary with **no Dock icon**. So this
skill builds the real bundle via `scripts/build-app.sh`. For a dev build the script
auto-picks a real codesigning identity from **your own** keychain (any "Apple
Development" / "Developer ID Application" cert — a free Apple ID gives you one) so the
dev app can post macOS notifications; `usernoted` rejects an ad-hoc signature outright,
so an unsigned dev build can **never** banner. A contributor with **no** signing cert
falls back to ad-hoc automatically — the build still succeeds, they just don't get
notifications. Set `SIGN_IDENTITY=…` to force a specific one. The app runs
unsandboxed with `.exec` PTYs — see `CLAUDE.md`. The icon is `packaging/icon-static.svg`,
rasterized to `packaging/AppIcon.png` by `scripts/render-icon.sh` (needs headless
Chrome). Note: dev and release currently share the same icon art and differ only by
name ("termio dev") — a tinted dev icon is a possible follow-up.

## Instructions

When invoked, execute these steps sequentially:

1. **Kill only the running dev app + its tunnel.** Both channels' inner binary is
   named `termio` (`CFBundleExecutable`), so `pkill -x termio` would kill the release
   app too. Match the dev bundle **path** instead. SIGKILL skips the app's
   `willTerminate` cleanup, so also reap the companion tunnel it spawned on the dev
   port (8788):
   ```bash
   pkill -9 -f "termio-dev.app/Contents/MacOS/termio" || true
   pkill -9 -f "cloudflared tunnel --url http://127.0.0.1:8788" || true
   pkill -9 -f "tunelo port 8788" || true
   ```

2. **Build the dev bundle** from the committed `packaging/AppIcon.png` — do *not*
   re-render the icon (see "Icons" below). Show the tail of the output; if the
   build fails, show the error and **stop — do NOT relaunch**:
   ```bash
   TERMIO_CHANNEL=dev ./scripts/build-app.sh 2>&1 | tail -12
   ```

3. **Re-register the dev bundle with LaunchServices, then refresh the Dock cache.**
   macOS resolves an app's icon by its bundle id (`sh.termio.app.dev`); `lsregister -f`
   forces our path to win and `killall Dock` drops the cached icon:
   ```bash
   LSREG="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
   "$LSREG" -f "$PWD/termio-dev.app"
   touch ./termio-dev.app
   killall Dock 2>/dev/null || true
   ```

4. **Relaunch via `open`** (NOT `nohup` on the inner binary — that bypasses
   LaunchServices). `open`'s `--stdout` / `--stderr` still capture logs:
   ```bash
   open ./termio-dev.app --stdout /tmp/termio-dev.log --stderr /tmp/termio-dev.log
   echo "launched ./termio-dev.app (logs: /tmp/termio-dev.log)"
   ```

5. **Upgrade the running dev daemon.** `build-app.sh` already rebuilt the
   daemon into `Resources/termiod`, but a **running** dev daemon keeps its old
   image: a dev app's launch-time reconcile deliberately refuses to stage
   (its bundle stamp is a placeholder, and that guard is what keeps a dev
   build from ever deploying itself over a release daemon). Without this
   step, a termiod-side change gets "verified" against the previous daemon.
   The deploy is idempotent — same version reports `current` and does
   nothing, so it costs nothing on Swift-only rebuilds — and an execve
   handoff keeps the pid and carries every live session. `TERMIOD_SOCK` must
   be unset first: a shell inside a termio session carries it pointed at the
   *release* daemon, and it overrides the channel.
   ```bash
   unset TERMIOD_SOCK
   TERMIO_CHANNEL=dev ./termio-dev.app/Contents/Resources/termiod deploy --json 2>&1 | tail -3
   ```
   A daemon that isn't running needs nothing: the first pane starts it from
   the new bundle.

6. **Report** the result: whether the build succeeded, the app relaunched, and
   the daemon version the deploy reported. If the window doesn't appear, check
   `/tmp/termio-dev.log`.

## Verifying a termiod-side change

Sessions carried across the handoff keep the environment and shell their
**old** daemon spawned them with — a change to session spawning (env,
injection, PTY setup) is invisible in them by design. Open a **new** session
to see it.

## Icons

`scripts/render-icon.sh` rasterizes `packaging/icon-static.svg` into
`packaging/AppIcon.png` (and the iOS asset) through headless Chrome — and its
output is **not reproducible**: rendering the same SVG twice writes two different
files, neither matching the committed one. Running it on every rebuild therefore
dirtied the tree with two unreviewable binary diffs, cost a Chrome launch, and
risked overwriting hand-touched artwork. So it is not part of this loop.

Re-render only when the SVG actually changed, and commit the PNGs as their own
change:

```bash
./scripts/render-icon.sh && git add packaging/AppIcon.png \
  ios/Sources/Assets.xcassets/AppIcon.appiconset/AppIcon.png
```

## Notes

- This builds **release configuration** (via `build-app.sh`) into the dev *channel*
  bundle, so it's a few seconds slower than a bare `swift build`. For quick code-only
  iteration without a bundle, run `swift build` and launch
  `"$(swift build --show-bin-path)/termio"` directly — but that has no Dock icon and,
  running unbundled, uses `AppChannel.suffix == ""` (i.e. the *release* state dir and
  port 8787), so it is **not** isolated. Use the bundle when you need isolation.
- A concurrent SwiftPM process holding the `.build` lock can make a build emit
  spurious errors mid-write; if that happens, just rerun.
- Do NOT modify `Package.swift` or sources during a rebuild.
- After rebuilding a UI change, pair this with the `app-screenshot-debug` skill to
  actually *see* the result.
