# 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-14 --- ## 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 5 ready. Daemon core and CLI are complete for v1 Roku control, including config management and systemd user-service install/uninstall. Next work is HTTP API parity. **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 ├── src/ │ ├── lib.rs ← Library surface for shared modules and integration tests │ ├── main.rs ← Binary entry point and runtime bootstrap │ ├── 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) ``` --- ## 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 - HTTP route handlers and request validation - CI/CD configuration - 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) |