Files
saphid--frame-control/ios/scripts/make_frame_bundle.py
T
saphidandClaude Opus 5.5 13187e8f6a iPhone and iPad app: the same features, served from the Frame
An iPhone can't run Python or ssh, but the Frame can. The app (ios/, SwiftUI)
connects with its own SSH key (Citadel), copies the server and helpers to
~/.cache/frame-control/<version> on the Frame once per version, starts
ui/server.py there with FRAME_LOCAL=1 on the Frame's 127.0.0.1, and shows the
page through an SSH tunnel. The server exits when the phone disconnects.

Server: FRAME_LOCAL=1 puts ui/local-bin on PATH, whose ssh stand-in runs each
`ssh frame COMMAND` locally (and serves as rsync's transport), so desktop and
phone share one code path. Android display goes through podman exec there, as
the Frame has no adb. FRAME_UI_KEY replaces the fixed X-Frame-UI value with a
per-session key. Power actions take the Developer Mode password via sudo -S.
--port 0 now prints the port it took.

Page: a bottom tab bar and safe areas on phones, Play buttons visible on touch
screens, saving through the share sheet, SSH/SFTP/Steam Link/remote desktop
opening in their iOS apps, and a password dialog for power.

App: pairing with the Developer Mode password once (never stored) or with a
key the user adds; host key pinned on first use; plain-language connection
errors with quiet retries; frame-control://install links; alerts and confirms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:18:15 +10:00

53 lines
1.8 KiB
Python

#!/usr/bin/env python3
"""Pack what Frame Control's server needs to run on the Frame itself (the files the
desktop app ships, plus ui/local-bin) into one reproducible .tar.gz.
The iPhone app copies it to ~/.cache/frame-control/<version> on the Frame and
starts ui/server.py there. <version> is the SHA-256 of the archive, so a new
build replaces an old one and an unchanged one isn't copied again.
Usage: make_frame_bundle.py OUT.tar.gz (prints the version)
"""
import gzip
import hashlib
import io
import sys
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PATTERNS = ["ui/*.py", "ui/*.html", "ui/local-bin/*", "scripts/*.sh", "frame/android/*.sh", "frame/android/*.py",
"frame/devkit-utils/**/*", "apk-catalog/*.py", "apk-catalog/pins.json", "apk-catalog/site/apps.js"]
def files():
found = set()
for pattern in PATTERNS:
for p in ROOT.glob(pattern):
if p.is_file() and "__pycache__" not in p.parts:
found.add(p)
return sorted(found)
def build():
raw = io.BytesIO()
with tarfile.open(fileobj=raw, mode="w", format=tarfile.PAX_FORMAT) as tar:
for p in files():
info = tarfile.TarInfo(str(p.relative_to(ROOT)))
data = p.read_bytes()
info.size, info.mtime, info.uid, info.gid, info.uname, info.gname = len(data), 0, 0, 0, "", ""
info.mode = 0o755 if p.stat().st_mode & 0o111 else 0o644
tar.addfile(info, io.BytesIO(data))
out = io.BytesIO()
with gzip.GzipFile(fileobj=out, mode="wb", mtime=0) as gz:
gz.write(raw.getvalue())
return out.getvalue()
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(__doc__)
data = build()
Path(sys.argv[1]).write_bytes(data)
print(hashlib.sha256(data).hexdigest()[:16])