1#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
13pub enum HostAdapter {
14 Claude,
15 Codex,
16 Hermes,
17 OpenClaw,
18}
19
20impl HostAdapter {
21 pub const ALL: &'static [Self] = &[Self::Claude, Self::Codex, Self::Hermes, Self::OpenClaw];
22
23 pub fn as_str(self) -> &'static str {
24 match self {
25 Self::Claude => "claude",
26 Self::Codex => "codex",
27 Self::Hermes => "hermes",
28 Self::OpenClaw => "openclaw",
29 }
30 }
31
32 pub fn from_id(name: &str) -> Option<Self> {
35 match name {
36 "claude" | "claude-code" => Some(Self::Claude),
37 "codex" => Some(Self::Codex),
38 "hermes" => Some(Self::Hermes),
39 "openclaw" => Some(Self::OpenClaw),
40 _ => None,
41 }
42 }
43
44 pub fn default_mode(self) -> IntegrationMode {
46 match self {
47 Self::Claude => IntegrationMode::NativeHook,
48 Self::Codex => IntegrationMode::ManualSkill,
49 Self::Hermes | Self::OpenClaw => IntegrationMode::ReferenceAdapter,
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
61pub enum IntegrationMode {
62 ManualSkill,
63 LauncherWrapper,
64 NativeHook,
65 ReferenceAdapter,
66}
67
68impl IntegrationMode {
69 pub const ALL: &'static [Self] = &[
70 Self::ManualSkill,
71 Self::LauncherWrapper,
72 Self::NativeHook,
73 Self::ReferenceAdapter,
74 ];
75
76 pub fn as_str(self) -> &'static str {
77 match self {
78 Self::ManualSkill => "manual_skill",
79 Self::LauncherWrapper => "launcher_wrapper",
80 Self::NativeHook => "native_hook",
81 Self::ReferenceAdapter => "reference_adapter",
82 }
83 }
84
85 pub fn from_id(value: &str) -> Option<Self> {
86 match value {
87 "manual_skill" => Some(Self::ManualSkill),
88 "launcher_wrapper" => Some(Self::LauncherWrapper),
89 "native_hook" => Some(Self::NativeHook),
90 "reference_adapter" => Some(Self::ReferenceAdapter),
91 _ => None,
92 }
93 }
94
95 pub fn from_lifecycle_mode(mode: crate::IntegrationMode) -> Option<Self> {
98 match mode {
99 crate::IntegrationMode::ManualSkill => Some(Self::ManualSkill),
100 crate::IntegrationMode::LauncherWrapper => Some(Self::LauncherWrapper),
101 crate::IntegrationMode::NativeHook => Some(Self::NativeHook),
102 crate::IntegrationMode::ReferenceAdapter => Some(Self::ReferenceAdapter),
103 crate::IntegrationMode::TelemetryOnly => None,
104 }
105 }
106}
107
108pub fn supports_mode(host: HostAdapter, mode: IntegrationMode) -> bool {
111 match host {
112 HostAdapter::Claude => mode == IntegrationMode::NativeHook,
113 HostAdapter::Codex => matches!(
114 mode,
115 IntegrationMode::ManualSkill
116 | IntegrationMode::LauncherWrapper
117 | IntegrationMode::NativeHook
118 ),
119 HostAdapter::Hermes | HostAdapter::OpenClaw => mode == IntegrationMode::ReferenceAdapter,
120 }
121}
122
123pub fn supported_modes(host: HostAdapter) -> &'static [IntegrationMode] {
126 match host {
127 HostAdapter::Claude => &[IntegrationMode::NativeHook],
128 HostAdapter::Codex => &[
129 IntegrationMode::ManualSkill,
130 IntegrationMode::LauncherWrapper,
131 IntegrationMode::NativeHook,
132 ],
133 HostAdapter::Hermes | HostAdapter::OpenClaw => &[IntegrationMode::ReferenceAdapter],
134 }
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct RenderedAsset {
148 pub relative_path: &'static str,
149 pub contents: String,
150 pub mode: Option<u32>,
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155pub enum AssetStatus {
156 Present,
158 Missing,
160 Drifted,
164 InvalidMode,
166 NotApplicable,
168}
169
170#[derive(Clone, Copy, Debug, Eq, PartialEq)]
172pub enum FileAction {
173 Installed,
175 Updated,
177 AlreadyPresent,
179}
180
181pub fn combine_actions(current: FileAction, next: FileAction) -> FileAction {
185 match (current, next) {
186 (FileAction::Updated, _) | (_, FileAction::Updated) => FileAction::Updated,
187 (FileAction::Installed, _) | (_, FileAction::Installed) => FileAction::Installed,
188 _ => FileAction::AlreadyPresent,
189 }
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct MergedFile {
198 pub existing: Option<String>,
199 pub rendered: String,
200}
201
202#[derive(Debug)]
205pub enum HostAssetError {
206 UnsupportedMode {
208 host: HostAdapter,
209 mode: IntegrationMode,
210 },
211 Malformed { reason: String },
214 Parse { reason: String },
216 Serialize { reason: String },
219}
220
221impl std::fmt::Display for HostAssetError {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 match self {
224 Self::UnsupportedMode { host, mode } => write!(
225 f,
226 "unsupported mode `{}` for {} (supported: {})",
227 mode.as_str(),
228 host.as_str(),
229 supported_modes(*host)
230 .iter()
231 .map(|m| m.as_str())
232 .collect::<Vec<_>>()
233 .join(", ")
234 ),
235 Self::Malformed { reason } => write!(f, "malformed managed file: {reason}"),
236 Self::Parse { reason } => write!(f, "parse error: {reason}"),
237 Self::Serialize { reason } => write!(f, "serialize error: {reason}"),
238 }
239 }
240}
241
242impl std::error::Error for HostAssetError {}
243
244pub const CLAUDE_SOURCE_SETTINGS: &str = ".ccd-hosts/claude/settings.json";
245pub const CLAUDE_TARGET_SETTINGS: &str = ".claude/settings.json";
246
247pub const CODEX_SOURCE_README: &str = ".ccd-hosts/codex/README.md";
248pub const CODEX_SOURCE_LAUNCHER: &str = ".ccd-hosts/codex/launcher.sh";
249pub const CODEX_SOURCE_CONFIG: &str = ".ccd-hosts/codex/config.toml";
250pub const CODEX_SOURCE_HOOKS: &str = ".ccd-hosts/codex/hooks.json";
251pub const CODEX_TARGET_LAUNCHER: &str = ".codex/ccd-launch.sh";
252pub const CODEX_TARGET_CONFIG: &str = ".codex/config.toml";
253pub const CODEX_TARGET_HOOKS: &str = ".codex/hooks.json";
254
255pub const OPENCLAW_SOURCE_ADAPTER: &str = ".ccd-hosts/openclaw/adapter.json";
256pub const OPENCLAW_TARGET_ADAPTER: &str = ".openclaw/ccd.json";
257
258pub const HERMES_SOURCE_ADAPTER: &str = ".ccd-hosts/hermes/adapter.json";
259pub const HERMES_TARGET_ADAPTER: &str = ".hermes/ccd.json";