Skip to main content

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::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    /// Optional filesystem/object storage configuration for workspace/session files
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub filesystem: Option<crate::configuration::config::ObjectStorageConfig>,
54}
55
56/// Build configuration for custom build commands
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct BuildConfig {
59    /// Build command to execute
60    pub command: String,
61    /// Working directory for build (optional, defaults to package root)
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub working_dir: Option<String>,
64    /// Environment variables for build (optional)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub env: Option<std::collections::HashMap<String, String>>,
67}
68
69impl DistriServerConfig {
70    pub fn has_entrypoints(&self) -> bool {
71        self.entrypoints.is_some()
72    }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(tag = "type")]
77#[serde(rename_all = "lowercase")]
78pub struct EntryPoints {
79    pub path: String,
80}
81
82impl EntryPoints {
83    /// Validate the entrypoint configuration
84    pub fn validate(&self) -> Result<()> {
85        if self.path.is_empty() {
86            return Err(anyhow!("TypeScript entrypoint path cannot be empty"));
87        }
88        // Basic validation - could add more checks here
89        Ok(())
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct EngineConfig {
95    pub engine: String,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct AuthorsConfig {
100    pub primary: String,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct RegistryConfig {
105    pub url: Option<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct LockFile {
110    pub packages: HashMap<String, String>,
111    pub sources: HashMap<String, String>,
112}
113
114impl DistriServerConfig {
115    /// Get the working directory with fallback chain: config -> DISTRI_HOME -> current_dir
116    pub fn get_working_directory(&self) -> Result<std::path::PathBuf> {
117        // Fallback to current directory
118        std::env::current_dir().map_err(|e| anyhow!("Failed to get current directory: {}", e))
119    }
120
121    pub async fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
122        let content = fs::read_to_string(path).await?;
123        let manifest: DistriServerConfig = toml::from_str(&content)?;
124        manifest.validate()?;
125        Ok(manifest)
126    }
127
128    /// Validate that the manifest has at least one of agents or entrypoints
129    pub fn validate(&self) -> Result<()> {
130        let has_agents = self.agents.as_ref().map_or(false, |a| !a.is_empty());
131        let has_entrypoints = self.entrypoints.is_some();
132
133        if !has_agents && !has_entrypoints {
134            return Err(anyhow!(
135                "Package '{}' must define either agents or entrypoints (for tools/workflows)",
136                self.name
137            ));
138        }
139
140        // Validate entrypoints if present
141        if let Some(entrypoints) = &self.entrypoints {
142            entrypoints.validate()?;
143        }
144
145        Ok(())
146    }
147
148    pub async fn save_to_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
149        let content = toml::to_string_pretty(self)?;
150        fs::write(path, content).await?;
151        Ok(())
152    }
153
154    pub fn new_minimal(package_name: String) -> Self {
155        Self {
156            name: package_name,
157            version: "0.1.0".to_string(),
158            description: None,
159            license: Some("Apache-2.0".to_string()),
160            agents: Some(vec![]),
161            entrypoints: None, // Entry points for TypeScript and WASM
162            distri: Some(EngineConfig {
163                engine: ">=0.1.2".to_string(),
164            }),
165            authors: None,
166            registry: None,
167            mcp_servers: None,
168            server: None,
169            stores: None,
170            model_settings: None,
171            analysis_model_settings: None,
172            keywords: None,
173            filesystem: None,
174        }
175    }
176    pub fn current_dir() -> Result<std::path::PathBuf> {
177        let current_dir = std::env::current_dir()?;
178        Ok(current_dir)
179    }
180
181    pub fn find_configuration_in_current_dir() -> Result<std::path::PathBuf> {
182        let current_dir = Self::current_dir()?;
183        println!("current_dir: {:?}", current_dir);
184        let manifest_path = current_dir.join("distri.toml");
185
186        if manifest_path.exists() {
187            Ok(manifest_path)
188        } else {
189            Err(anyhow!("No distri.toml found in current directory"))
190        }
191    }
192}
193
194impl LockFile {
195    pub async fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
196        let content = fs::read_to_string(path).await?;
197        let lock_file: LockFile = toml::from_str(&content)?;
198        Ok(lock_file)
199    }
200
201    pub async fn save_to_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
202        let content = toml::to_string_pretty(self)?;
203        fs::write(path, content).await?;
204        Ok(())
205    }
206
207    pub fn new() -> Self {
208        Self {
209            packages: HashMap::new(),
210            sources: HashMap::new(),
211        }
212    }
213}
214
215impl Default for LockFile {
216    fn default() -> Self {
217        Self::new()
218    }
219}