1use std::env;
2use std::fmt;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{Error, config_err};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Actor {
13 pub name: String,
15 pub kind: ActorKind,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum ActorKind {
23 Human,
25 Agent,
27 Automation,
29}
30
31impl ActorKind {
32 #[must_use]
34 pub fn as_str(self) -> &'static str {
35 match self {
36 Self::Human => "human",
37 Self::Agent => "agent",
38 Self::Automation => "automation",
39 }
40 }
41}
42
43impl fmt::Display for ActorKind {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str(self.as_str())
46 }
47}
48
49impl std::str::FromStr for ActorKind {
50 type Err = Error;
51
52 fn from_str(text: &str) -> Result<Self, Self::Err> {
53 match text {
54 "human" => Ok(Self::Human),
55 "agent" => Ok(Self::Agent),
56 "automation" => Ok(Self::Automation),
57 other => Err(Error::Config(format!("unknown actor kind: {other}"))),
58 }
59 }
60}
61
62#[derive(Debug, Deserialize)]
63struct ActorFile {
64 actor: Option<ActorSection>,
65}
66
67#[derive(Debug, Deserialize)]
68struct ActorSection {
69 name: String,
70 kind: ActorKind,
71}
72
73pub fn resolve_actor() -> Result<Actor, Error> {
80 let Some(path) = actor_config_path() else {
81 return Err(Error::NoActorConfigured);
82 };
83 if !path.is_file() {
84 return Err(Error::NoActorConfigured);
85 }
86 let text = fs::read_to_string(&path)?;
87 let parsed: ActorFile = toml::from_str(&text).map_err(config_err)?;
88 match parsed.actor {
89 Some(section) => Ok(Actor {
90 name: section.name,
91 kind: section.kind,
92 }),
93 None => Err(Error::NoActorConfigured),
94 }
95}
96
97fn actor_config_path() -> Option<PathBuf> {
98 if let Ok(home) = env::var("ATELIER_CONFIG_HOME") {
99 return Some(PathBuf::from(home).join("config.toml"));
100 }
101 if let Ok(xdg) = env::var("XDG_CONFIG_HOME") {
102 return Some(PathBuf::from(xdg).join("atelier").join("config.toml"));
103 }
104 if let Ok(home) = env::var("HOME") {
105 return Some(
106 PathBuf::from(home)
107 .join(".config")
108 .join("atelier")
109 .join("config.toml"),
110 );
111 }
112 None
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct Source {
121 pub kind: SourceKind,
123 pub path: PathBuf,
126 pub sync: SyncPolicy,
128 pub mount: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub branch: Option<String>,
136}
137
138pub(crate) const ROOT_MOUNT: &str = "/";
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "kebab-case")]
144pub enum SourceKind {
145 LocalFolder,
147 LocalGit,
150 Remote,
153}
154
155impl fmt::Display for SourceKind {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 Self::LocalFolder => f.write_str("local-folder"),
159 Self::LocalGit => f.write_str("local-git"),
160 Self::Remote => f.write_str("remote"),
161 }
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "kebab-case")]
168pub enum SyncPolicy {
169 TwoWay,
171}
172
173impl fmt::Display for SyncPolicy {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self {
176 Self::TwoWay => f.write_str("two-way"),
177 }
178 }
179}
180
181#[derive(Debug, Serialize, Deserialize)]
183pub struct WorkspaceConfig {
184 pub schema: u32,
185 pub workspace: WorkspaceSection,
186 #[serde(default)]
187 pub landing: LandingPolicy,
188 #[serde(default)]
189 pub journal: JournalPolicy,
190 #[serde(default, rename = "source")]
191 pub sources: Vec<Source>,
192}
193
194#[derive(Debug, Serialize, Deserialize)]
195pub struct WorkspaceSection {
196 pub name: String,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(default)]
204pub struct LandingPolicy {
205 pub approvals: u32,
207 pub allow_self_approve: bool,
209 pub dismiss_approvals_on_new_snapshots: bool,
211}
212
213impl Default for LandingPolicy {
214 fn default() -> Self {
215 Self {
216 approvals: 1,
217 allow_self_approve: true,
218 dismiss_approvals_on_new_snapshots: true,
219 }
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(default)]
227pub struct JournalPolicy {
228 pub instruction_fidelity: InstructionFidelity,
230}
231
232impl Default for JournalPolicy {
233 fn default() -> Self {
234 Self {
235 instruction_fidelity: InstructionFidelity::Summary,
236 }
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "lowercase")]
244pub enum InstructionFidelity {
245 Summary,
247 Verbatim,
249}
250
251impl WorkspaceConfig {
252 pub fn new(name: String) -> Self {
253 Self {
254 schema: 1,
255 workspace: WorkspaceSection { name },
256 landing: LandingPolicy::default(),
257 journal: JournalPolicy::default(),
258 sources: Vec::new(),
259 }
260 }
261}
262
263pub fn read_workspace_config(atelier_dir: &Path) -> Result<WorkspaceConfig, Error> {
265 let text = fs::read_to_string(atelier_dir.join("config.toml"))?;
266 toml::from_str(&text).map_err(config_err)
267}
268
269pub fn write_workspace_config(atelier_dir: &Path, config: &WorkspaceConfig) -> Result<(), Error> {
271 let text = toml::to_string(config).map_err(config_err)?;
272 fs::write(atelier_dir.join("config.toml"), text)?;
273 Ok(())
274}