use std::collections::BTreeSet;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HostKind {
ClaudeCode,
ClaudeDesktop,
Codex,
CodexCli,
#[serde(rename = "opencode")]
OpenCode,
#[serde(rename = "opencode-cli")]
OpenCodeCli,
Hermes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HostAppKind {
DesktopApp,
CliTool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum HostProduct {
Claude,
Codex,
OpenCode,
Hermes,
}
impl HostKind {
pub const ALL: [Self; 7] = [
Self::ClaudeCode,
Self::ClaudeDesktop,
Self::Codex,
Self::CodexCli,
Self::OpenCode,
Self::OpenCodeCli,
Self::Hermes,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ClaudeCode => "claude-code",
Self::ClaudeDesktop => "claude-desktop",
Self::Codex => "codex",
Self::CodexCli => "codex-cli",
Self::OpenCode => "opencode",
Self::OpenCodeCli => "opencode-cli",
Self::Hermes => "hermes",
}
}
#[must_use]
pub const fn kind(self) -> HostAppKind {
match self {
Self::ClaudeDesktop | Self::Codex | Self::OpenCode => HostAppKind::DesktopApp,
Self::ClaudeCode | Self::CodexCli | Self::OpenCodeCli | Self::Hermes => {
HostAppKind::CliTool
},
}
}
#[must_use]
pub const fn product(self) -> HostProduct {
match self {
Self::ClaudeCode | Self::ClaudeDesktop => HostProduct::Claude,
Self::Codex | Self::CodexCli => HostProduct::Codex,
Self::OpenCode | Self::OpenCodeCli => HostProduct::OpenCode,
Self::Hermes => HostProduct::Hermes,
}
}
#[must_use]
pub const fn shares_config_with(self) -> Option<Self> {
match self {
Self::Codex => Some(Self::CodexCli),
Self::CodexCli => Some(Self::Codex),
Self::OpenCode => Some(Self::OpenCodeCli),
Self::OpenCodeCli => Some(Self::OpenCode),
Self::ClaudeCode | Self::ClaudeDesktop | Self::Hermes => None,
}
}
pub fn parse_list<S: AsRef<str>>(hosts: &[S]) -> Result<Vec<Self>, UnknownHostKind> {
hosts.iter().map(|host| host.as_ref().parse()).collect()
}
pub fn validate_targets<S: AsRef<str>>(hosts: &[S]) -> Result<(), UnknownHostKind> {
hosts
.iter()
.map(S::as_ref)
.filter(|host| *host != COWORK_TARGET)
.try_for_each(|host| host.parse::<Self>().map(drop))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InstanceHosts {
pub enabled: Vec<HostKind>,
pub opt_in: BTreeSet<HostKind>,
}
impl InstanceHosts {
#[must_use]
pub fn is_opt_in(&self, host: HostKind) -> bool {
self.opt_in.contains(&host)
}
pub fn by_default(&self) -> impl Iterator<Item = HostKind> + '_ {
self.enabled
.iter()
.copied()
.filter(|host| !self.is_opt_in(*host))
}
}
pub const COWORK_TARGET: &str = "cowork";
impl HostProduct {
#[must_use]
pub fn targeted_by<S: AsRef<str>>(self, hosts: &[S]) -> bool {
hosts.iter().any(|host| {
host.as_ref()
.parse::<HostKind>()
.is_ok_and(|kind| kind.product() == self)
})
}
pub fn hosts(self) -> impl Iterator<Item = HostKind> {
HostKind::ALL
.into_iter()
.filter(move |host| host.product() == self)
}
}
impl fmt::Display for HostKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Display for HostAppKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::DesktopApp => "desktop-app",
Self::CliTool => "cli-tool",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown host id `{0}`")]
pub struct UnknownHostKind(pub String);
impl FromStr for HostKind {
type Err = UnknownHostKind;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|kind| kind.as_str() == s)
.ok_or_else(|| UnknownHostKind(s.to_owned()))
}
}