distri_types/configuration/
manifest.rs

1use anyhow::{Result, anyhow};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5use tokio::fs;
6
7// Import config types
8use crate::agent::{BrowserHooksConfig, ModelSettings};
9use crate::configuration::config::{ExternalMcpServer, ServerConfig, StoreConfig};
10
11/// User configuration from distri.toml file
12#[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    // Entry points for TypeScript and WASM
25    #[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    // Configuration that was previously in Configuration
36    #[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    /// Optional filesystem/object storage configuration for workspace/session files
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub filesystem: Option<crate::configuration::config::ObjectStorageConfig>,
56}
57
58/// Build configuration for custom build commands
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct BuildConfig {
61    /// Build command to execute
62    pub command: String,
63    /// Working directory for build (optional, defaults to package root)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub working_dir: Option<String>,
66    /// Environment variables for build (optional)
67    #[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    /// Validate the entrypoint configuration
86    pub fn validate(&self) -> Result<()> {
87        if self.path.is_empty() {
88            return Err(anyhow!("TypeScript entrypoint path cannot be empty"));
89        }
90        // Basic validation - could add more checks here
91        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    /// Get the working directory with fallback chain: config -> DISTRI_HOME -> current_dir
118    pub fn get_working_directory(&self) -> Result<std::path::PathBuf> {
119        // Fallback to current directory
120        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    /// Validate that the manifest has at least one of agents or entrypoints
131    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        // Validate entrypoints if present
143        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, // Entry points for TypeScript and WASM
164            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}