summaryrefslogtreecommitdiff
path: root/makima/daemon/src/config.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/daemon/src/config.rs
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/daemon/src/config.rs')
-rw-r--r--makima/daemon/src/config.rs550
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
- }
- }
-}