diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/daemon/src/config.rs | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/daemon/src/config.rs')
| -rw-r--r-- | makima/daemon/src/config.rs | 550 |
1 files changed, 0 insertions, 550 deletions
diff --git a/makima/daemon/src/config.rs b/makima/daemon/src/config.rs deleted file mode 100644 index 94d1e8a..0000000 --- a/makima/daemon/src/config.rs +++ /dev/null @@ -1,550 +0,0 @@ -//! Configuration management for the makima daemon. - -use config::{Config, Environment, File}; -use serde::Deserialize; -use std::collections::HashMap; -use std::path::PathBuf; - -/// Root daemon configuration. -#[derive(Debug, Clone, Deserialize)] -pub struct DaemonConfig { - /// Server connection settings. - #[serde(default)] - pub server: ServerConfig, - - /// Worktree settings. - #[serde(default)] - pub worktree: WorktreeConfig, - - /// Process settings. - #[serde(default)] - pub process: ProcessConfig, - - /// Local database settings. - #[serde(default)] - pub local_db: LocalDbConfig, - - /// Logging settings. - #[serde(default)] - pub logging: LoggingConfig, - - /// Repositories to auto-clone on startup. - #[serde(default)] - pub repos: ReposConfig, -} - -/// Server connection configuration. -#[derive(Debug, Clone, Deserialize)] -#[serde(default)] -pub struct ServerConfig { - /// WebSocket URL of makima server (e.g., ws://localhost:8080 or wss://makima.example.com). - /// Defaults to wss://api.makima.jp. - #[serde(default = "default_server_url")] - pub url: String, - - /// API key for authentication. - #[serde(default, alias = "apikey")] - pub api_key: String, - - /// Heartbeat interval in seconds. - #[serde(default = "default_heartbeat_interval", alias = "heartbeatintervalsecs")] - pub heartbeat_interval_secs: u64, - - /// Reconnect interval in seconds after connection loss. - #[serde(default = "default_reconnect_interval", alias = "reconnectintervalsecs")] - pub reconnect_interval_secs: u64, - - /// Maximum reconnect attempts before giving up (0 = infinite). - #[serde(default, alias = "maxreconnectattempts")] - pub max_reconnect_attempts: u32, -} - -fn default_heartbeat_interval() -> u64 { - 30 -} - -fn default_reconnect_interval() -> u64 { - 5 -} - -fn default_server_url() -> String { - "wss://api.makima.jp".to_string() -} - -impl Default for ServerConfig { - fn default() -> Self { - Self { - url: default_server_url(), - api_key: String::new(), - heartbeat_interval_secs: default_heartbeat_interval(), - reconnect_interval_secs: default_reconnect_interval(), - max_reconnect_attempts: 0, - } - } -} - -/// Worktree configuration for task isolation. -#[derive(Debug, Clone, Deserialize)] -#[serde(default)] -pub struct WorktreeConfig { - /// Base directory for worktrees (~/.makima/worktrees). - #[serde(default = "default_worktree_base_dir", alias = "basedir")] - pub base_dir: PathBuf, - - /// Base directory for cloned repositories (~/.makima/repos). - #[serde(default = "default_repos_base_dir", alias = "reposdir")] - pub repos_dir: PathBuf, - - /// Branch prefix for task branches. - #[serde(default = "default_branch_prefix", alias = "branchprefix")] - pub branch_prefix: String, - - /// Clean up worktrees on daemon start. - #[serde(default, alias = "cleanuponstart")] - pub cleanup_on_start: bool, - - /// Default target repository path for pushing completed branches. - /// Used when task.target_repo_path is not set. - #[serde(default, alias = "defaulttargetrepo")] - pub default_target_repo: Option<PathBuf>, -} - -fn default_worktree_base_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".makima") - .join("worktrees") -} - -fn default_repos_base_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".makima") - .join("repos") -} - -fn default_branch_prefix() -> String { - "makima/task-".to_string() -} - -impl Default for WorktreeConfig { - fn default() -> Self { - Self { - base_dir: default_worktree_base_dir(), - repos_dir: default_repos_base_dir(), - branch_prefix: default_branch_prefix(), - cleanup_on_start: false, - default_target_repo: None, - } - } -} - -/// Process configuration for Claude Code subprocess execution. -#[derive(Debug, Clone, Deserialize)] -#[serde(default)] -pub struct ProcessConfig { - /// Path or command for Claude Code CLI. - #[serde(default = "default_claude_command", alias = "claudecommand")] - pub claude_command: String, - - /// Additional arguments to pass to Claude Code. - /// These are added after the default arguments. - #[serde(default, alias = "claudeargs")] - pub claude_args: Vec<String>, - - /// Arguments to pass before the default arguments. - /// Useful for overriding defaults. - #[serde(default, alias = "claudepreargs")] - pub claude_pre_args: Vec<String>, - - /// Skip the --dangerously-skip-permissions flag (default: false). - /// Set to true if you want to use Claude's permission system. - #[serde(default, alias = "enablepermissions")] - pub enable_permissions: bool, - - /// Skip the --verbose flag (default: false). - #[serde(default, alias = "disableverbose")] - pub disable_verbose: bool, - - /// Maximum concurrent tasks. - #[serde(default = "default_max_tasks", alias = "maxconcurrenttasks")] - pub max_concurrent_tasks: u32, - - /// Default timeout for tasks in seconds (0 = no timeout). - #[serde(default, alias = "defaulttimeoutsecs")] - pub default_timeout_secs: u64, - - /// Additional environment variables to pass to Claude Code. - #[serde(default, alias = "envvars")] - pub env_vars: HashMap<String, String>, -} - -fn default_claude_command() -> String { - "claude".to_string() -} - -fn default_max_tasks() -> u32 { - 4 -} - -impl Default for ProcessConfig { - fn default() -> Self { - Self { - claude_command: default_claude_command(), - claude_args: Vec::new(), - claude_pre_args: Vec::new(), - enable_permissions: false, - disable_verbose: false, - max_concurrent_tasks: default_max_tasks(), - default_timeout_secs: 0, - env_vars: HashMap::new(), - } - } -} - -/// Local database configuration. -#[derive(Debug, Clone, Deserialize)] -#[serde(default)] -pub struct LocalDbConfig { - /// Path to local SQLite database. - #[serde(default = "default_db_path")] - pub path: PathBuf, -} - -impl Default for LocalDbConfig { - fn default() -> Self { - Self { - path: default_db_path(), - } - } -} - -fn default_db_path() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".makima") - .join("daemon.db") -} - -/// Logging configuration. -#[derive(Debug, Clone, Deserialize, Default)] -pub struct LoggingConfig { - /// Log level: "trace", "debug", "info", "warn", "error". - #[serde(default = "default_log_level")] - pub level: String, - - /// Log format: "pretty" or "json". - #[serde(default = "default_log_format")] - pub format: String, -} - -fn default_log_level() -> String { - "info".to_string() -} - -fn default_log_format() -> String { - "pretty".to_string() -} - -/// Repository auto-clone configuration. -#[derive(Debug, Clone, Deserialize, Default)] -pub struct ReposConfig { - /// Directory to clone repositories into (default: ~/.makima/home). - #[serde(default = "default_home_dir")] - pub home_dir: PathBuf, - - /// List of repositories to auto-clone on startup. - /// Each entry can be a URL (e.g., "https://github.com/user/repo.git") - /// or a shorthand (e.g., "github:user/repo"). - #[serde(default, alias = "autoclone")] - pub auto_clone: Vec<RepoEntry>, -} - -/// A repository entry for auto-cloning. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -pub enum RepoEntry { - /// Simple URL string. - Url(String), - /// Detailed configuration. - Config { - /// Repository URL. - url: String, - /// Custom directory name (defaults to repo name from URL). - #[serde(default)] - name: Option<String>, - /// Branch to checkout after cloning (defaults to default branch). - #[serde(default)] - branch: Option<String>, - /// Whether to do a shallow clone (default: false). - #[serde(default)] - shallow: bool, - }, -} - -impl RepoEntry { - /// Get the URL for this repo entry. - pub fn url(&self) -> &str { - match self { - RepoEntry::Url(url) => url, - RepoEntry::Config { url, .. } => url, - } - } - - /// Get the custom name, if any. - pub fn name(&self) -> Option<&str> { - match self { - RepoEntry::Url(_) => None, - RepoEntry::Config { name, .. } => name.as_deref(), - } - } - - /// Get the branch to checkout, if any. - pub fn branch(&self) -> Option<&str> { - match self { - RepoEntry::Url(_) => None, - RepoEntry::Config { branch, .. } => branch.as_deref(), - } - } - - /// Whether to do a shallow clone. - pub fn shallow(&self) -> bool { - match self { - RepoEntry::Url(_) => false, - RepoEntry::Config { shallow, .. } => *shallow, - } - } - - /// Get the directory name to use (either custom name or derived from URL). - pub fn dir_name(&self) -> Option<String> { - if let Some(name) = self.name() { - return Some(name.to_string()); - } - - // Derive from URL - let url = self.url(); - - // Handle shorthand formats - let url = if url.starts_with("github:") { - url.strip_prefix("github:").unwrap_or(url) - } else if url.starts_with("gitlab:") { - url.strip_prefix("gitlab:").unwrap_or(url) - } else { - url - }; - - // Extract repo name from URL - url.trim_end_matches('/') - .trim_end_matches(".git") - .rsplit('/') - .next() - .map(|s| s.to_string()) - } - - /// Expand the URL (e.g., convert shorthand to full URL). - pub fn expanded_url(&self) -> String { - let url = self.url(); - - if url.starts_with("github:") { - format!("https://github.com/{}.git", url.strip_prefix("github:").unwrap_or("")) - } else if url.starts_with("gitlab:") { - format!("https://gitlab.com/{}.git", url.strip_prefix("gitlab:").unwrap_or("")) - } else { - url.to_string() - } - } -} - -fn default_home_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".makima") - .join("home") -} - -impl DaemonConfig { - /// Load configuration from files and environment variables. - /// - /// Configuration sources (in order of precedence): - /// 1. Environment variables (MAKIMA_API_KEY, MAKIMA_DAEMON_SERVER_URL, etc.) - /// 2. ./makima-daemon.toml (current directory) - /// 3. ~/.config/makima-daemon/config.toml - /// 4. /etc/makima-daemon/config.toml (Linux only) - /// - /// Environment variable examples: - /// - MAKIMA_API_KEY=your-api-key (preferred) - /// - MAKIMA_DAEMON_SERVER_URL=ws://localhost:8080 - /// - MAKIMA_DAEMON_PROCESS_MAXCONCURRENTTASKS=4 - pub fn load() -> Result<Self, config::ConfigError> { - Self::load_from_path(None) - } - - /// Load configuration from a specific path plus standard sources. - fn load_from_path(config_path: Option<&std::path::Path>) -> Result<Self, config::ConfigError> { - let mut builder = Config::builder(); - - // System-wide config (Linux only) - #[cfg(target_os = "linux")] - { - builder = builder.add_source( - File::with_name("/etc/makima-daemon/config").required(false), - ); - } - - // User config - if let Some(config_dir) = dirs::config_dir() { - let user_config = config_dir.join("makima-daemon").join("config"); - builder = builder.add_source( - File::with_name(user_config.to_str().unwrap_or("")).required(false), - ); - } - - // Local config - builder = builder.add_source(File::with_name("makima-daemon").required(false)); - - // Custom config file (if provided) - if let Some(path) = config_path { - builder = builder.add_source( - File::with_name(path.to_str().unwrap_or("")).required(true), - ); - } - - // Environment variables with underscore separator for nesting - // e.g., MAKIMA_DAEMON_SERVER_URL -> server.url - // MAKIMA_DAEMON_SERVER_APIKEY -> server.api_key - builder = builder.add_source( - Environment::with_prefix("MAKIMA_DAEMON") - .separator("_") - .try_parsing(true), - ); - - let config = builder.build()?; - let mut config: DaemonConfig = config.try_deserialize()?; - - // Check for MAKIMA_API_KEY environment variable (preferred over MAKIMA_DAEMON_SERVER_APIKEY) - if let Ok(api_key) = std::env::var("MAKIMA_API_KEY") { - config.server.api_key = api_key; - } - - // Validate required fields (don't validate here - let load_with_cli do final validation) - Ok(config) - } - - /// Validate that required configuration fields are set. - pub fn validate(&self) -> Result<(), config::ConfigError> { - if self.server.api_key.is_empty() { - return Err(config::ConfigError::Message( - "API key is required. Set via MAKIMA_API_KEY, config file, or --api-key".to_string() - )); - } - Ok(()) - } - - /// Load configuration with CLI argument overrides. - /// - /// Configuration sources (in order of precedence, highest first): - /// 1. CLI arguments - /// 2. Environment variables - /// 3. Custom config file (if --config specified) - /// 4. ./makima-daemon.toml (current directory) - /// 5. ~/.config/makima-daemon/config.toml - /// 6. /etc/makima-daemon/config.toml (Linux only) - /// 7. Default values - pub fn load_with_cli(cli: &crate::cli::Cli) -> Result<Self, config::ConfigError> { - // Load base config (with optional custom config file) - let mut config = Self::load_from_path(cli.config.as_deref())?; - - // Apply CLI overrides (highest priority) - if let Some(ref repos_dir) = cli.repos_dir { - config.worktree.repos_dir = repos_dir.clone(); - } - if let Some(ref worktrees_dir) = cli.worktrees_dir { - config.worktree.base_dir = worktrees_dir.clone(); - } - if let Some(ref server_url) = cli.server_url { - config.server.url = server_url.clone(); - } - if let Some(ref api_key) = cli.api_key { - config.server.api_key = api_key.clone(); - } - if let Some(max_tasks) = cli.max_tasks { - config.process.max_concurrent_tasks = max_tasks; - } - // Log level is always set (has default) - config.logging.level = cli.log_level.clone(); - - // Validate required fields after all sources are merged - config.validate()?; - - Ok(config) - } - - /// Create a minimal config for testing. - #[cfg(test)] - pub fn test_config() -> Self { - Self { - server: ServerConfig { - url: "ws://localhost:8080".to_string(), - api_key: "test-key".to_string(), - heartbeat_interval_secs: 30, - reconnect_interval_secs: 5, - max_reconnect_attempts: 0, - }, - worktree: WorktreeConfig { - base_dir: PathBuf::from("/tmp/makima-daemon-test/worktrees"), - repos_dir: PathBuf::from("/tmp/makima-daemon-test/repos"), - branch_prefix: "makima/task-".to_string(), - cleanup_on_start: true, - default_target_repo: None, - }, - process: ProcessConfig { - claude_command: "claude".to_string(), - claude_args: Vec::new(), - claude_pre_args: Vec::new(), - enable_permissions: false, - disable_verbose: false, - max_concurrent_tasks: 2, - default_timeout_secs: 0, - env_vars: HashMap::new(), - }, - local_db: LocalDbConfig { - path: PathBuf::from("/tmp/makima-daemon-test/state.db"), - }, - logging: LoggingConfig::default(), - repos: ReposConfig::default(), - } - } -} - -/// Helper module for dirs crate (minimal subset). -mod dirs { - use std::path::PathBuf; - - pub fn home_dir() -> Option<PathBuf> { - std::env::var("HOME").ok().map(PathBuf::from) - } - - pub fn config_dir() -> Option<PathBuf> { - #[cfg(target_os = "macos")] - { - std::env::var("HOME") - .ok() - .map(|h| PathBuf::from(h).join("Library").join("Application Support")) - } - #[cfg(target_os = "linux")] - { - std::env::var("XDG_CONFIG_HOME") - .ok() - .map(PathBuf::from) - .or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".config"))) - } - #[cfg(target_os = "windows")] - { - std::env::var("APPDATA").ok().map(PathBuf::from) - } - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { - None - } - } -} |
