1use std::env;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq, Eq, Default)]
10pub enum TaskMode {
11 #[default]
13 AgentOnly,
14 All,
16}
17
18impl TaskMode {
19 fn from_env_value(val: &str) -> Self {
21 match val.trim().to_lowercase().as_str() {
22 "all" => Self::All,
23 _ => Self::AgentOnly,
24 }
25 }
26}
27
28#[derive(Debug, Clone, Default)]
34pub struct Config {
35 pub mode: TaskMode,
37 pub justfile_path: Option<String>,
39 pub allowed_dirs: Vec<PathBuf>,
42 pub init_template_file: Option<String>,
44}
45
46impl Config {
47 pub fn load() -> Self {
51 let _ = dotenvy::from_filename(".task-mcp.env");
53
54 let mode = env::var("TASK_MCP_MODE")
55 .map(|v| TaskMode::from_env_value(&v))
56 .unwrap_or_default();
57
58 let justfile_path = env::var("TASK_MCP_JUSTFILE").ok();
59
60 let allowed_dirs = env::var("TASK_MCP_ALLOWED_DIRS")
61 .map(|v| parse_allowed_dirs(&v))
62 .unwrap_or_default();
63
64 let init_template_file = env::var("TASK_MCP_INIT_TEMPLATE_FILE").ok();
65
66 Self {
67 mode,
68 justfile_path,
69 allowed_dirs,
70 init_template_file,
71 }
72 }
73
74 pub fn is_workdir_allowed(&self, workdir: &Path) -> bool {
79 if self.allowed_dirs.is_empty() {
80 return true;
81 }
82 self.allowed_dirs.iter().any(|d| workdir.starts_with(d))
83 }
84}
85
86fn parse_allowed_dirs(raw: &str) -> Vec<PathBuf> {
91 raw.split(',')
92 .map(str::trim)
93 .filter(|s| !s.is_empty())
94 .filter_map(|entry| match std::fs::canonicalize(entry) {
95 Ok(p) => Some(p),
96 Err(e) => {
97 eprintln!("task-mcp: TASK_MCP_ALLOWED_DIRS: skipping {entry:?}: {e}");
98 None
99 }
100 })
101 .collect()
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn task_mode_default_is_agent_only() {
110 assert_eq!(TaskMode::default(), TaskMode::AgentOnly);
111 }
112
113 #[test]
114 fn task_mode_from_env_value_all() {
115 assert_eq!(TaskMode::from_env_value("all"), TaskMode::All);
116 assert_eq!(TaskMode::from_env_value("ALL"), TaskMode::All);
117 assert_eq!(TaskMode::from_env_value("All"), TaskMode::All);
118 }
119
120 #[test]
121 fn task_mode_from_env_value_agent_only() {
122 assert_eq!(TaskMode::from_env_value("agent-only"), TaskMode::AgentOnly);
123 assert_eq!(TaskMode::from_env_value("unknown"), TaskMode::AgentOnly);
124 assert_eq!(TaskMode::from_env_value(""), TaskMode::AgentOnly);
125 }
126
127 #[test]
128 fn config_default() {
129 let cfg = Config::default();
130 assert_eq!(cfg.mode, TaskMode::AgentOnly);
131 assert!(cfg.justfile_path.is_none());
132 assert!(cfg.allowed_dirs.is_empty());
133 }
134
135 #[test]
136 fn is_workdir_allowed_empty_allows_all() {
137 let cfg = Config {
138 allowed_dirs: vec![],
139 ..Config::default()
140 };
141 assert!(cfg.is_workdir_allowed(Path::new("/any/path")));
142 assert!(cfg.is_workdir_allowed(Path::new("/")));
143 }
144
145 #[test]
146 fn is_workdir_allowed_match() {
147 let cfg = Config {
148 allowed_dirs: vec![PathBuf::from("/home/user/projects")],
149 ..Config::default()
150 };
151 assert!(cfg.is_workdir_allowed(Path::new("/home/user/projects")));
152 assert!(cfg.is_workdir_allowed(Path::new("/home/user/projects/foo")));
153 assert!(cfg.is_workdir_allowed(Path::new("/home/user/projects/foo/bar")));
154 }
155
156 #[test]
157 fn is_workdir_allowed_no_match() {
158 let cfg = Config {
159 allowed_dirs: vec![PathBuf::from("/home/user/projects")],
160 ..Config::default()
161 };
162 assert!(!cfg.is_workdir_allowed(Path::new("/home/user/other")));
164 assert!(!cfg.is_workdir_allowed(Path::new("/home/user/projects-extra")));
166 assert!(!cfg.is_workdir_allowed(Path::new("/home/user")));
167 }
168}