Skip to main content

wsx_core/integration/
model.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(rename_all = "kebab-case")]
7pub enum IntegrationTarget {
8    Pi,
9    Omp,
10    Claude,
11    Codex,
12    Copilot,
13    Devin,
14    Droid,
15    Kimi,
16    Opencode,
17    Kilo,
18    Hermes,
19    Qodercli,
20    Qwen,
21    Cursor,
22    Mastracode,
23    AntigravityCli,
24    Grok,
25}
26
27impl IntegrationTarget {
28    pub const ALL: [Self; 17] = [
29        Self::Pi,
30        Self::Omp,
31        Self::Claude,
32        Self::Codex,
33        Self::Copilot,
34        Self::Devin,
35        Self::Droid,
36        Self::Kimi,
37        Self::Opencode,
38        Self::Kilo,
39        Self::Hermes,
40        Self::Qodercli,
41        Self::Qwen,
42        Self::Cursor,
43        Self::Mastracode,
44        Self::AntigravityCli,
45        Self::Grok,
46    ];
47
48    pub const fn cli_value(self) -> &'static str {
49        match self {
50            Self::Pi => "pi",
51            Self::Omp => "omp",
52            Self::Claude => "claude",
53            Self::Codex => "codex",
54            Self::Copilot => "copilot",
55            Self::Devin => "devin",
56            Self::Droid => "droid",
57            Self::Kimi => "kimi",
58            Self::Opencode => "opencode",
59            Self::Kilo => "kilo",
60            Self::Hermes => "hermes",
61            Self::Qodercli => "qodercli",
62            Self::Qwen => "qwen",
63            Self::Cursor => "cursor",
64            Self::Mastracode => "mastracode",
65            Self::AntigravityCli => "antigravity-cli",
66            Self::Grok => "grok",
67        }
68    }
69
70    pub const fn label(self) -> &'static str {
71        match self {
72            Self::Pi => "Pi",
73            Self::Omp => "OMP",
74            Self::Claude => "Claude Code",
75            Self::Codex => "Codex",
76            Self::Copilot => "GitHub Copilot CLI",
77            Self::Devin => "Devin CLI",
78            Self::Droid => "Factory Droid",
79            Self::Kimi => "Kimi Code",
80            Self::Opencode => "OpenCode",
81            Self::Kilo => "Kilo Code",
82            Self::Hermes => "Hermes Agent",
83            Self::Qodercli => "Qoder CLI",
84            Self::Qwen => "Qwen Code",
85            Self::Cursor => "Cursor Agent",
86            Self::Mastracode => "MastraCode",
87            Self::AntigravityCli => "Antigravity CLI",
88            Self::Grok => "Grok CLI",
89        }
90    }
91
92    pub const fn lifecycle(self) -> LifecycleCapability {
93        match self {
94            Self::Pi
95            | Self::Omp
96            | Self::Claude
97            | Self::Codex
98            | Self::Kimi
99            | Self::Opencode
100            | Self::Kilo
101            | Self::Mastracode => LifecycleCapability::Authoritative,
102            _ => LifecycleCapability::IdentityOnly,
103        }
104    }
105
106    pub const fn expected_version(self) -> u32 {
107        match self {
108            Self::Pi => 15,
109            Self::Omp => 12,
110            Self::Claude => 14,
111            Self::Codex => 11,
112            Self::Copilot | Self::Droid | Self::Qodercli => 5,
113            Self::Devin => 4,
114            Self::Mastracode => 5,
115            Self::Kimi => 10,
116            Self::Opencode => 13,
117            Self::Kilo => 7,
118            Self::Hermes => 7,
119            Self::Qwen | Self::Cursor | Self::Grok => 3,
120            Self::AntigravityCli => 3,
121        }
122    }
123}
124
125impl fmt::Display for IntegrationTarget {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter.write_str(self.cli_value())
128    }
129}
130
131impl FromStr for IntegrationTarget {
132    type Err = String;
133
134    fn from_str(value: &str) -> Result<Self, Self::Err> {
135        Self::ALL
136            .into_iter()
137            .find(|target| target.cli_value() == value)
138            .ok_or_else(|| format!("unknown integration target: {value}"))
139    }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum LifecycleCapability {
145    Authoritative,
146    IdentityOnly,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "snake_case")]
151pub enum InstallStatus {
152    Missing,
153    Current,
154    Outdated,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct IntegrationMetadata {
159    pub target: IntegrationTarget,
160    pub cli_value: &'static str,
161    pub label: &'static str,
162    pub lifecycle: LifecycleCapability,
163    pub available: bool,
164    pub compatible: bool,
165    pub compatibility_note: Option<&'static str>,
166    pub install_status: InstallStatus,
167    pub installed_version: Option<u32>,
168    pub expected_version: u32,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct InstallResult {
173    pub target: IntegrationTarget,
174    pub paths: Vec<std::path::PathBuf>,
175}