Files
tvctl/PROJECT_MAP.md
T
44r0n7 642fa716d1 feat: finish Roku adapter dev-mode support
Add digest-auth sideload installs, debugger log capture, and live Roku
integration coverage so the full Roku milestone is validated on hardware.
2026-04-14 09:35:26 -04:00

11 KiB

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 2 complete. Roku adapter is live-validated; daemon and CLI wiring are next. 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
│   │   ├── 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.

pub trait TvAdapter: Send + Sync {
    async fn discover(&self) -> Result<Vec<DeviceInfo>>;
    async fn state(&self, device: &Device) -> Result<DeviceState>;
    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<TvKey>) -> Result<()>;
    async fn list_apps(&self, device: &Device) -> Result<Vec<AppInfo>>;
    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<Vec<String>> {
        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

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<Utc>,
    pub last_seen: DateTime<Utc>,
}

DeviceState

pub struct DeviceState {
    pub device_id: Uuid,
    pub power: PowerState,      // On | Off | Unknown
    pub active_app: Option<AppInfo>,
    pub volume: Option<VolumeInfo>,
    pub timestamp: DateTime<Utc>,
}

AppInfo

pub struct AppInfo {
    pub id: String,             // tvctl normalized id
    pub name: String,
    pub version: Option<String>,
    pub platform_id: String,    // raw platform value
}

TvKey (normalized key enum)

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 <ip> --platform <platform>
│   ├── select <name>
│   ├── info <name>
│   └── remove <name>
├── app
│   ├── list
│   ├── launch <name|id>
│   ├── stop
│   └── refresh
├── remote
│   ├── key <key>
│   └── sequence <key> [key...]
├── state
├── dev
│   ├── install <zip>
│   ├── reload
│   └── logs
└── config
    ├── list
    ├── get <key>
    ├── set <key> <value>
    ├── reset
    └── reload

Global flags: --device <name|id>  --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):

{ "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

[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

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

  • Daemon runtime, socket transport, and persistence logic
  • HTTP route handlers and request validation
  • Real CLI command handling beyond skeleton parsing
  • 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)