1use super::agent::{default_orchestrator_rules, AgentConfig};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8pub(super) fn default_runtime() -> String {
9 "tmux".into()
10}
11pub(super) fn default_agent() -> String {
12 "claude-code".into()
13}
14pub(super) fn default_workspace() -> String {
15 "worktree".into()
16}
17pub(super) fn default_tracker() -> String {
18 "github".into()
19}
20
21fn default_true() -> bool {
22 true
23}
24fn is_true(b: &bool) -> bool {
25 *b
26}
27
28fn default_prevent_idle_sleep() -> bool {
29 cfg!(target_os = "macos")
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ScmWebhookConfig {
40 #[serde(default = "default_true", skip_serializing_if = "is_true")]
41 pub enabled: bool,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub path: Option<String>,
44 #[serde(
45 default,
46 skip_serializing_if = "Option::is_none",
47 rename = "secretEnvVar",
48 alias = "secret_env_var"
49 )]
50 pub secret_env_var: Option<String>,
51 #[serde(
52 default,
53 skip_serializing_if = "Option::is_none",
54 rename = "signatureHeader",
55 alias = "signature_header"
56 )]
57 pub signature_header: Option<String>,
58 #[serde(
59 default,
60 skip_serializing_if = "Option::is_none",
61 rename = "eventHeader",
62 alias = "event_header"
63 )]
64 pub event_header: Option<String>,
65 #[serde(
66 default,
67 skip_serializing_if = "Option::is_none",
68 rename = "deliveryHeader",
69 alias = "delivery_header"
70 )]
71 pub delivery_header: Option<String>,
72 #[serde(
73 default,
74 skip_serializing_if = "Option::is_none",
75 rename = "maxBodyBytes",
76 alias = "max_body_bytes"
77 )]
78 pub max_body_bytes: Option<u64>,
79}
80
81impl Default for ScmWebhookConfig {
82 fn default() -> Self {
83 Self {
84 enabled: true,
85 path: None,
86 secret_env_var: None,
87 signature_header: None,
88 event_header: None,
89 delivery_header: None,
90 max_body_bytes: None,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
97pub struct PluginConfig {
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub plugin: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub package: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub path: Option<String>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub webhook: Option<ScmWebhookConfig>,
107 #[serde(flatten, default)]
108 pub extra: HashMap<String, serde_yaml::Value>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct PowerConfig {
114 #[serde(
115 default = "default_prevent_idle_sleep",
116 rename = "preventIdleSleep",
117 alias = "prevent_idle_sleep"
118 )]
119 pub prevent_idle_sleep: bool,
120}
121
122impl Default for PowerConfig {
123 fn default() -> Self {
124 Self {
125 prevent_idle_sleep: cfg!(target_os = "macos"),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct RoleAgentConfig {
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub agent: Option<String>,
136
137 #[serde(
139 default,
140 skip_serializing_if = "Option::is_none",
141 rename = "agent_config",
142 alias = "agentConfig"
143 )]
144 pub agent_config: Option<AgentConfig>,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct DefaultsConfig {
150 #[serde(default = "default_runtime")]
151 pub runtime: String,
152 #[serde(default = "default_agent")]
153 pub agent: String,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub orchestrator: Option<RoleAgentConfig>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub worker: Option<RoleAgentConfig>,
159 #[serde(
161 default,
162 skip_serializing_if = "Option::is_none",
163 rename = "orchestrator_rules",
164 alias = "orchestratorRules",
165 alias = "orchestrator-rules"
166 )]
167 pub orchestrator_rules: Option<String>,
168 #[serde(default = "default_workspace")]
169 pub workspace: String,
170 #[serde(default = "default_tracker")]
171 pub tracker: String,
172 #[serde(
180 default,
181 skip_serializing_if = "Option::is_none",
182 rename = "branch_namespace",
183 alias = "branchNamespace",
184 alias = "branch-namespace"
185 )]
186 pub branch_namespace: Option<String>,
187 #[serde(default)]
188 pub notifiers: Vec<String>,
189}
190
191impl Default for DefaultsConfig {
192 fn default() -> Self {
193 Self {
194 runtime: default_runtime(),
195 agent: default_agent(),
196 orchestrator: None,
197 worker: None,
198 orchestrator_rules: Some(default_orchestrator_rules().to_string()),
199 workspace: default_workspace(),
200 tracker: default_tracker(),
201 branch_namespace: None,
202 notifiers: vec![],
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn defaults_config_roundtrip() {
213 let dc = DefaultsConfig::default();
214 assert_eq!(dc.runtime, "tmux");
215 assert_eq!(dc.agent, "claude-code");
216 assert_eq!(dc.workspace, "worktree");
217 assert_eq!(dc.tracker, "github");
218 assert!(dc.notifiers.is_empty());
219
220 let yaml = serde_yaml::to_string(&dc).unwrap();
221 let dc2: DefaultsConfig = serde_yaml::from_str(&yaml).unwrap();
222 assert_eq!(dc, dc2);
223 }
224
225 #[test]
226 fn power_config_default_is_platform_aware() {
227 let pc = PowerConfig::default();
228 if cfg!(target_os = "macos") {
229 assert!(
230 pc.prevent_idle_sleep,
231 "macOS: prevent_idle_sleep should default to true"
232 );
233 } else {
234 assert!(
235 !pc.prevent_idle_sleep,
236 "non-macOS: prevent_idle_sleep should default to false"
237 );
238 }
239 }
240}