Skip to main content

lilo_rm_core/
version.rs

1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6pub const RUNTIME_PROTOCOL_VERSION: &str = "0.6";
7
8pub const RUNTIME_PROTOCOL_CAPABILITIES: &[RuntimeCapability] = &[
9    RuntimeCapability::StructuredProtocolErrors,
10    RuntimeCapability::HeadlessStdioLogPaths,
11    RuntimeCapability::StatusSessionSetFilter,
12    RuntimeCapability::StatusUpdatedSinceFilter,
13    RuntimeCapability::TypedNudgeOutcomes,
14    RuntimeCapability::ValidateTargetPreflight,
15    RuntimeCapability::EventsCursor,
16    RuntimeCapability::EventsLongPoll,
17    RuntimeCapability::TmuxPaneSnapshot,
18    RuntimeCapability::KillOutcomes,
19    RuntimeCapability::SpawnConflicts,
20];
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23pub struct VersionInfo {
24    pub version: String,
25    pub git_sha: String,
26    pub protocol_version: String,
27    pub capabilities: Vec<RuntimeCapability>,
28}
29
30impl VersionInfo {
31    pub fn new(version: impl Into<String>, git_sha: impl Into<String>) -> Self {
32        Self {
33            version: version.into(),
34            git_sha: git_sha.into(),
35            protocol_version: RUNTIME_PROTOCOL_VERSION.to_owned(),
36            capabilities: RUNTIME_PROTOCOL_CAPABILITIES.to_vec(),
37        }
38    }
39}
40
41pub fn version_info() -> VersionInfo {
42    VersionInfo::new(env!("CARGO_PKG_VERSION"), env!("RTM_GIT_SHA"))
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum RuntimeCapability {
47    /// Error responses expose stable machine readable codes.
48    StructuredProtocolErrors,
49    /// Headless spawn responses include stdout and stderr log paths.
50    HeadlessStdioLogPaths,
51    /// Status requests accept a set of session ids.
52    StatusSessionSetFilter,
53    /// Status requests accept an updated time lower bound.
54    StatusUpdatedSinceFilter,
55    /// Nudge responses expose typed delivery outcomes.
56    TypedNudgeOutcomes,
57    /// ValidateTarget checks a target string without spawning.
58    ValidateTargetPreflight,
59    /// Events support durable cursor replay.
60    EventsCursor,
61    /// Events requests accept a bounded long poll wait window.
62    EventsLongPoll,
63    /// Tmux targets support on demand pane snapshot capture.
64    TmuxPaneSnapshot,
65    /// Kill responses include typed Signalled or AlreadyExited outcomes.
66    KillOutcomes,
67    /// Spawn rejects session reuse and occupied tmux panes with typed conflicts.
68    SpawnConflicts,
69}
70
71impl RuntimeCapability {
72    pub const fn as_str(self) -> &'static str {
73        match self {
74            Self::StructuredProtocolErrors => "structured_protocol_errors",
75            Self::HeadlessStdioLogPaths => "headless_stdio_log_paths",
76            Self::StatusSessionSetFilter => "status_session_set_filter",
77            Self::StatusUpdatedSinceFilter => "status_updated_since_filter",
78            Self::TypedNudgeOutcomes => "typed_nudge_outcomes",
79            Self::ValidateTargetPreflight => "validate_target_preflight",
80            Self::EventsCursor => "events_cursor",
81            Self::EventsLongPoll => "events_long_poll",
82            Self::TmuxPaneSnapshot => "tmux_pane_snapshot",
83            Self::KillOutcomes => "kill_outcomes",
84            Self::SpawnConflicts => "spawn_conflicts",
85        }
86    }
87}
88
89impl Display for RuntimeCapability {
90    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
91        formatter.write_str(self.as_str())
92    }
93}
94
95impl FromStr for RuntimeCapability {
96    type Err = String;
97
98    fn from_str(value: &str) -> Result<Self, Self::Err> {
99        match value {
100            "structured_protocol_errors" => Ok(Self::StructuredProtocolErrors),
101            "headless_stdio_log_paths" => Ok(Self::HeadlessStdioLogPaths),
102            "status_session_set_filter" => Ok(Self::StatusSessionSetFilter),
103            "status_updated_since_filter" => Ok(Self::StatusUpdatedSinceFilter),
104            "typed_nudge_outcomes" => Ok(Self::TypedNudgeOutcomes),
105            "validate_target_preflight" => Ok(Self::ValidateTargetPreflight),
106            "events_cursor" => Ok(Self::EventsCursor),
107            "events_long_poll" => Ok(Self::EventsLongPoll),
108            "tmux_pane_snapshot" => Ok(Self::TmuxPaneSnapshot),
109            "kill_outcomes" => Ok(Self::KillOutcomes),
110            "spawn_conflicts" => Ok(Self::SpawnConflicts),
111            other => Err(format!("unknown runtime capability {other}")),
112        }
113    }
114}
115
116impl Serialize for RuntimeCapability {
117    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
118    where
119        S: Serializer,
120    {
121        serializer.serialize_str(self.as_str())
122    }
123}
124
125impl<'de> Deserialize<'de> for RuntimeCapability {
126    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127    where
128        D: Deserializer<'de>,
129    {
130        String::deserialize(deserializer)?
131            .parse()
132            .map_err(serde::de::Error::custom)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{RUNTIME_PROTOCOL_VERSION, VersionInfo};
139
140    #[test]
141    fn protocol_version_advertises_v06_spawn_conflict_contract() {
142        assert_eq!(RUNTIME_PROTOCOL_VERSION, "0.6");
143        assert_eq!(VersionInfo::new("rtm", "git").protocol_version, "0.6");
144    }
145}