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.
This commit is contained in:
44r0n7
2026-04-14 10:46:41 -04:00
parent 29e53d16b0
commit 8bf0a94416
9 changed files with 1268 additions and 67 deletions
+80 -5
View File
@@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
use tokio::fs;
use uuid::Uuid;
use crate::adapters::{Device, DeviceInfo, TvAdapter, roku::RokuAdapter};
use crate::{
adapters::{AppInfo, Device, DeviceInfo, DeviceState, TvAdapter, TvKey, roku::RokuAdapter},
daemon::config::TvctlConfig,
};
/// The persisted collection of known devices.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -187,6 +190,17 @@ impl Default for AdapterRegistry {
}
impl AdapterRegistry {
/// Build the adapter registry from the loaded daemon config.
pub fn from_config(config: &TvctlConfig) -> Self {
let username =
(!config.dev.roku_username.is_empty()).then(|| config.dev.roku_username.clone());
let password =
(!config.dev.roku_password.is_empty()).then(|| config.dev.roku_password.clone());
Self {
roku: RokuAdapter::with_dev_credentials(username, password),
}
}
/// Return the supported platform names.
pub fn supported_platforms(&self) -> Vec<&'static str> {
vec!["roku"]
@@ -222,15 +236,76 @@ impl AdapterRegistry {
}
/// Return apps from a concrete device using its platform adapter.
pub async fn list_apps(
&self,
device: &Device,
) -> anyhow::Result<Vec<crate::adapters::AppInfo>> {
pub async fn list_apps(&self, device: &Device) -> anyhow::Result<Vec<AppInfo>> {
match device.platform.as_str() {
"roku" => Ok(self.roku.list_apps(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Fetch the current state for a concrete device.
pub async fn state(&self, device: &Device) -> anyhow::Result<DeviceState> {
match device.platform.as_str() {
"roku" => Ok(self.roku.state(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Launch an app on a concrete device.
pub async fn launch(&self, device: &Device, app: &str) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.launch(device, app).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Stop the currently running app on a concrete device.
pub async fn stop_app(&self, device: &Device) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.stop_app(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Send a single normalized key to a concrete device.
pub async fn key(&self, device: &Device, key: TvKey) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.key(device, key).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Send a normalized key sequence to a concrete device.
pub async fn sequence(&self, device: &Device, keys: Vec<TvKey>) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.sequence(device, keys).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Install a development package on a concrete device.
pub async fn dev_install(&self, device: &Device, zip: &[u8]) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.dev_install(device, zip).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Reload the active development package on a concrete device.
pub async fn dev_reload(&self, device: &Device) -> anyhow::Result<()> {
match device.platform.as_str() {
"roku" => Ok(self.roku.dev_reload(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
/// Fetch development logs from a concrete device.
pub async fn dev_logs(&self, device: &Device) -> anyhow::Result<Vec<String>> {
match device.platform.as_str() {
"roku" => Ok(self.roku.dev_logs(device).await?),
other => anyhow::bail!("unsupported platform '{other}'"),
}
}
}
fn matches_target(device: &Device, target: &str) -> bool {