# PROJECT_MAP.md # tvctl — Living Project Map # This file is the single source of truth for any agent working on this project. # Read this first. Update this when you make structural changes. # Last updated: 2026-04-18 --- ## What This Project Is `tvctl` is a Rust CLI tool and daemon (`tvctld`) that provides a unified, brand-agnostic control layer for smart TVs. It exposes a CLI and a local HTTP API so that technical users and tool builders can control any supported TV without caring about the underlying platform. **One sentence:** A local-first daemon and CLI that lets technical users and tool builders script and control smart TVs through a stable, brand-agnostic API. --- ## Project Status **Phase:** Milestone 6 in progress. Daemon core, CLI, and `/v1` HTTP API are complete for v1 Roku control. Remaining work is release prep (service verification, cross-compile, CI, first release). **Platform v1:** Roku only (via ECP HTTP API) **Language:** Rust **Crate type:** Binary (single binary distribution target) --- ## Directory Structure ``` tvctl/ ├── .gitignore ← Repository hygiene and runtime artifact exclusions ├── AGENTS.md ← Compatibility shim pointing to AGENT.md ├── README.md ← User-facing documentation (complete) ├── PROJECT_MAP.md ← This file. Agent context. Always read first. ├── AGENT.md ← Instructions for AI agents working on this project ├── ROADMAP.md ← Feature roadmap and milestone tracking ├── Cargo.toml ← Crate manifest and dependency definitions ├── Cargo.lock ← Locked dependency graph ├── docs/ │ ├── DESIGN.md ← Full design decisions and rationale │ ├── API.md ← HTTP API specification (detailed) │ └── ADAPTER.md ← Adapter trait spec and implementation guide ├── examples/ │ └── http-dashboard/ ← Static browser dashboard for /v1 API endpoint testing ├── src/ │ ├── lib.rs ← Library surface for shared modules and integration tests │ ├── main.rs ← Binary entry point and runtime bootstrap │ ├── logging.rs ← Tracing initialization and runtime log-filter reload │ ├── cli/ ← CLI layer (clap-based scaffold) │ │ └── mod.rs │ ├── daemon/ ← tvctld daemon core scaffolding │ │ ├── mod.rs │ │ ├── config.rs ← Config loading and runtime path helpers │ │ ├── ipc.rs ← Unix socket request/response protocol │ │ ├── registry.rs ← Device registry │ │ ├── discovery.rs ← SSDP discovery service │ │ ├── cache.rs ← App cache manager │ │ └── state.rs ← State cache │ ├── api/ ← HTTP API server scaffold (axum) │ │ └── mod.rs │ └── adapters/ ← Platform adapters and shared types │ ├── mod.rs ← Adapter trait definition and core data shapes │ └── roku/ ← Roku ECP adapter implementation │ └── mod.rs ├── tests/ │ └── roku_live.rs ← Live Roku integration tests gated by env vars └── cache/ ← Runtime cache (gitignored) ``` --- ## Core Design Decisions These are final. Do not re-open without explicit instruction. | Decision | Choice | Reason | |----------|--------|--------| | Language | Rust | Developer familiarity, single binary, async ecosystem | | CLI framework | clap | Best-in-class for Rust, shell completions, polished help | | HTTP framework | axum | Mature, tokio-native, clean routing | | CLI↔daemon transport | Unix socket | Secure, no port conflicts, user-only access | | Tool builder transport | HTTP API | Stable, versioned, JSON | | Config format | TOML | Rust ecosystem standard, human-readable | | CLI pattern | resource-verb | Consistent, mirrors REST, scales well | | API versioning | /v1/ from day one | Stability signal to tool builders | | App cache strategy | Organic growth from live TV data | Accurate, small, zero maintenance | | State cache | In-memory only, cleared on restart | Honest — never pretend to have stale state | | Device registry | Persisted to devices.json | Survives restarts | | Default HTTP bind | 127.0.0.1 | Secure by default | | Key naming | kebab-case normalized | Unix convention, adapter handles translation | | Systemd | User-level service, not system | Runs as user, not root | --- ## Architecture ``` CLI (tvctl) └── Unix Socket └── tvctld daemon ├── Core Router ├── Device Registry (devices.json) ├── App Cache (cache/{platform}.apps.json) ├── State Cache (in-memory) ├── Discovery Service (SSDP) ├── Adapter Registry │ └── Roku Adapter (ECP/HTTP) └── HTTP API Server (:7272) └── /v1/... (tool builders) ``` The CLI reaches the daemon over the Unix socket. The HTTP API runs in the same process and dispatches directly into the daemon request executor instead of looping back through the socket layer. --- ## Adapter Trait The central abstraction. All platform implementations must satisfy this trait. Defined in `src/adapters/mod.rs`. ```rust pub trait TvAdapter: Send + Sync { async fn discover(&self) -> Result>; async fn state(&self, device: &Device) -> Result; async fn launch(&self, device: &Device, app: &str) -> Result<()>; async fn stop_app(&self, device: &Device) -> Result<()>; async fn key(&self, device: &Device, key: TvKey) -> Result<()>; async fn sequence(&self, device: &Device, keys: Vec) -> Result<()>; async fn list_apps(&self, device: &Device) -> Result>; async fn dev_install(&self, device: &Device, zip: &[u8]) -> Result<()> { Err(TvError::NotSupported("dev_install")) } async fn dev_reload(&self, device: &Device) -> Result<()> { Err(TvError::NotSupported("dev_reload")) } async fn dev_logs(&self, device: &Device) -> Result> { Err(TvError::NotSupported("dev_logs")) } } ``` Dev methods have default implementations that return `NotSupported`. Platforms that support them override. Nothing above the adapter layer needs to know which platforms support which features. --- ## Data Shapes ### Device ```rust pub struct Device { pub id: Uuid, pub name: String, // user-assigned friendly name pub original_name: String, // name as reported by device at discovery pub platform: String, // "roku" | "googletv" | "firetv" pub address: IpAddr, pub port: u16, pub is_default: bool, pub discovered_at: DateTime, pub last_seen: DateTime, } ``` ### DeviceState ```rust pub struct DeviceState { pub device_id: Uuid, pub power: PowerState, // On | Off | Unknown pub active_app: Option, pub volume: Option, pub timestamp: DateTime, } ``` ### AppInfo ```rust pub struct AppInfo { pub id: String, // tvctl normalized id pub name: String, pub version: Option, pub platform_id: String, // raw platform value } ``` ### TvKey (normalized key enum) ```rust pub enum TvKey { Home, Back, Up, Down, Left, Right, Select, Play, Pause, PlayPause, Stop, Rewind, FastForward, Replay, Skip, ChannelUp, ChannelDown, VolumeUp, VolumeDown, Mute, Power, PowerOn, PowerOff, InputHdmi1, InputHdmi2, InputHdmi3, InputHdmi4, InputAv, InputTuner, Search, Info, Options, Literal(String), // for text input } ``` --- ## CLI Command Tree ``` tvctl ├── (bare) full help output ├── daemon │ ├── start │ ├── stop │ ├── restart │ ├── status │ ├── install generate + enable systemd user service │ └── uninstall ├── device │ ├── list │ ├── discover │ ├── add --address --platform │ ├── select │ ├── info │ └── remove ├── app │ ├── list │ ├── launch │ ├── stop │ └── refresh ├── remote │ └── send [key...] ├── state ├── dev │ ├── install │ ├── reload │ └── logs └── config ├── list ├── get ├── set ├── reset └── reload Global flags: --device --json --help --version ``` --- ## HTTP API Surface Base: `http://127.0.0.1:7272/v1` ``` GET /v1/devices POST /v1/devices/discover GET /v1/devices/{id} DELETE /v1/devices/{id} GET /v1/devices/{id}/state GET /v1/devices/{id}/apps POST /v1/devices/{id}/apps/launch body: {"app": "netflix"} POST /v1/devices/{id}/apps/stop POST /v1/devices/{id}/apps/refresh POST /v1/devices/{id}/remote/key body: {"key": "home"} POST /v1/devices/{id}/remote/sequence body: {"keys": ["home", "down"]} POST /v1/devices/{id}/dev/install body: multipart zip POST /v1/devices/{id}/dev/reload GET /v1/devices/{id}/dev/logs GET /v1/daemon/status GET /v1/config PATCH /v1/config POST /v1/config/reload ``` Response envelope (always): ```json { "ok": true, "data": { ... } } { "ok": false, "error": { "code": "...", "message": "...", "hint": "..." } } ``` --- ## Storage Layout ``` ~/.config/tvctl/config.toml ← user config (TOML) ~/.local/share/tvctl/devices.json ← device registry (daemon managed) ~/.local/share/tvctl/cache/ roku.apps.json ← organic app cache per platform googletv.apps.json firetv.apps.json /run/user/{uid}/tvctl.sock ← unix socket (runtime) ``` --- ## Config Schema ```toml [daemon] socket = "/run/user/1000/tvctl.sock" http_enabled = true http_port = 7272 http_host = "127.0.0.1" log_level = "info" [discovery] auto_discover = true interval_secs = 300 timeout_secs = 5 [devices] default = "" [dev] enabled = true roku_username = "" roku_password = "" ``` --- ## Platform Support | Platform | Status | Protocol | |----------|--------|----------| | Roku | v1 target | ECP (HTTP on port 8060) | | Google TV | post-MVP | ADB | | Fire TV | post-MVP | ADB | | Samsung | deprioritized | Tizen (unstable app IDs) | --- ## Known Risks | Risk | Severity | Mitigation | |------|----------|------------| | Roku ECP changes without notice | High/Low-likelihood | Adapter isolation | | SSDP blocked on some networks | Medium | Manual device add fallback | | App name resolution ambiguity | Low | Raw --id flag fallback | | State staleness | Low | Honest timestamps on all state | | Unix socket permissions | Medium | 0600, user-level systemd service | | Platform creep | Medium | Roku-first discipline in v1 | --- ## What Has NOT Been Started - CI/CD configuration - Must pin Rust toolchain >= 1.85 because `edition = "2024"` is in use. - Cross-compile validation for `x86_64` and `aarch64` - Release/packaging --- ## Glossary | Term | Meaning | |------|---------| | ECP | External Control Protocol — Roku's HTTP control API on port 8060 | | SSDP | Simple Service Discovery Protocol — used for device discovery on LAN | | Adapter | A platform-specific implementation of the TvAdapter trait | | Registry | The persisted list of known devices (devices.json) | | Platform cache | Per-platform app list cache (grows from live TV data) | | Device cache | Per-device installed app list (subset of platform cache) |