8bf0a94416
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.
358 lines
11 KiB
Markdown
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) |
|