chore: scaffold tvctl foundation
Set up the Rust crate, baseline module layout, and project docs so the repository matches the design bundle and builds cleanly as a starting point.
This commit is contained in:
+354
@@ -0,0 +1,354 @@
|
||||
# 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 1 scaffolded. Foundation compiles; runtime logic not started.
|
||||
**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/
|
||||
│ ├── 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 scaffold
|
||||
│ └── mod.rs
|
||||
└── 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- Roku ECP transport and device discovery logic
|
||||
- Daemon runtime, socket transport, and persistence logic
|
||||
- HTTP route handlers and request validation
|
||||
- Real CLI command handling beyond skeleton parsing
|
||||
- Any tests
|
||||
- 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) |
|
||||
Reference in New Issue
Block a user