Files
tvctl/PROJECT_MAP.md
T
44r0n7 8bf0a94416 feat: complete CLI milestone
Finish the Milestone 4 CLI surface with config management, daemon
install and uninstall helpers, config reload handling, and final
polish for secret redaction and running-socket tracking.
2026-04-14 10:46:41 -04:00

358 lines
11 KiB
Markdown

# 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<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
```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<Utc>,
pub last_seen: DateTime<Utc>,
}
```
### DeviceState
```rust
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
```rust
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)
```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 <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):
```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) |