distri_types/configuration/
manifest.rs1use anyhow::{Result, anyhow};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5use tokio::fs;
6
7use crate::agent::{BrowserHooksConfig, ModelSettings};
9use crate::configuration::config::{ExternalMcpServer, ServerConfig, StoreConfig};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct DistriServerConfig {
14 pub name: String,
15 pub version: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub description: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub license: Option<String>,
20
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub agents: Option<Vec<String>>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub entrypoints: Option<EntryPoints>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub distri: Option<EngineConfig>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub authors: Option<AuthorsConfig>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub registry: Option<RegistryConfig>,
34
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub mcp_servers: Option<Vec<ExternalMcpServer>>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub server: Option<ServerConfig>,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub model_settings: Option<ModelSettings>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub analysis_model_settings: Option<ModelSettings>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub keywords: Option<Vec<String>>,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub stores: Option<StoreConfig>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub hooks: Option<BrowserHooksConfig>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub filesystem: Option<crate::configuration::config::ObjectStorageConfig>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct BuildConfig {
61 pub command: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub working_dir: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub env: Option<std::collections::HashMap<String, String>>,
69}
70
71impl DistriServerConfig {
72 pub fn has_entrypoints(&self) -> bool {
73 self.entrypoints.is_some()
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78#[serde(tag = "type")]
79#[serde(rename_all = "lowercase")]
80pub struct EntryPoints {
81 pub path: String,
82}
83
84impl EntryPoints {
85 pub fn validate(&self) -> Result<()> {
87 if self.path.is_empty() {
88 return Err(anyhow!("TypeScript entrypoint path cannot be empty"));
89 }
90 Ok(())
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct EngineConfig {
97 pub engine: String,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct AuthorsConfig {
102 pub primary: String,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct RegistryConfig {
107 pub url: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct LockFile {
112 pub packages: HashMap<String, String>,
113 pub sources: HashMap<String, String>,
114}
115
116impl DistriServerConfig {
117 pub fn get_working_directory(&self) -> Result<std::path::PathBuf> {
119 std::env::current_dir().map_err(|e| anyhow!("Failed to get current directory: {}", e))
121 }
122
123 pub async fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
124 let content = fs::read_to_string(path).await?;
125 let manifest: DistriServerConfig = toml::from_str(&content)?;
126 manifest.validate()?;
127 Ok(manifest)
128 }
129
130 pub fn validate(&self) -> Result<()> {
132 let has_agents = self.agents.as_ref().map_or(false, |a| !a.is_empty());
133 let has_entrypoints = self.entrypoints.is_some();
134
135 if !has_agents && !has_entrypoints {
136 return Err(anyhow!(
137 "Package '{}' must define either agents or entrypoints (for tools/workflows)",
138 self.name
139 ));
140 }
141
142 if let Some(entrypoints) = &self.entrypoints {
144 entrypoints.validate()?;
145 }
146
147 Ok(())
148 }
149
150 pub async fn save_to_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
151 let content = toml::to_string_pretty(self)?;
152 fs::write(path, content).await?;
153 Ok(())
154 }
155
156 pub fn new_minimal(package_name: String) -> Self {
157 Self {
158 name: package_name,
159 version: "0.1.0".to_string(),
160 description: None,
161 license: Some("Apache-2.0".to_string()),
162 agents: Some(vec![]),
163 entrypoints: None, distri: Some(EngineConfig {
165 engine: ">=0.1.2".to_string(),
166 }),
167 authors: None,
168 registry: None,
169 mcp_servers: None,
170 server: None,
171 stores: None,
172 model_settings: None,
173 analysis_model_settings: None,
174 keywords: None,
175 hooks: None,
176 filesystem: None,
177 }
178 }
179 pub fn current_dir() -> Result<std::path::PathBuf> {
180 let current_dir = std::env::current_dir()?;
181 Ok(current_dir)
182 }
183
184 pub fn find_configuration_in_current_dir() -> Result<std::path::PathBuf> {
185 let current_dir = Self::current_dir()?;
186 println!("current_dir: {:?}", current_dir);
187 let manifest_path = current_dir.join("distri.toml");
188
189 if manifest_path.exists() {
190 Ok(manifest_path)
191 } else {
192 Err(anyhow!("No distri.toml found in current directory"))
193 }
194 }
195}
196
197impl LockFile {
198 pub async fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
199 let content = fs::read_to_string(path).await?;
200 let lock_file: LockFile = toml::from_str(&content)?;
201 Ok(lock_file)
202 }
203
204 pub async fn save_to_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
205 let content = toml::to_string_pretty(self)?;
206 fs::write(path, content).await?;
207 Ok(())
208 }
209
210 pub fn new() -> Self {
211 Self {
212 packages: HashMap::new(),
213 sources: HashMap::new(),
214 }
215 }
216}
217
218impl Default for LockFile {
219 fn default() -> Self {
220 Self::new()
221 }
222}