1use std::path::Path;
10
11use crate::error::{ConfigError, MarsError};
12
13use super::output;
14
15#[derive(Debug, clap::Args)]
17pub struct InitArgs {
18 pub target: Option<String>,
20
21 #[arg(long, value_name = "DIR")]
23 pub link: Vec<String>,
24}
25
26fn validate_target(target: &str) -> Result<(), MarsError> {
28 if target.contains('/') || target.contains('\\') {
29 return Err(MarsError::Config(ConfigError::Invalid {
30 message: format!(
31 "`{target}` looks like a path — TARGET should be a directory name \
32 like `.agents` or `.claude`. Use `--root` to specify project root."
33 ),
34 }));
35 }
36 if target == "." || target == ".." || target.is_empty() {
37 return Err(MarsError::Config(ConfigError::Invalid {
38 message: format!(
39 "`{target}` is not a valid target name — use a directory name like `.agents` or `.claude`."
40 ),
41 }));
42 }
43 Ok(())
44}
45
46fn ensure_consumer_config(project_root: &Path) -> Result<bool, MarsError> {
47 let config_path = project_root.join("mars.toml");
48 if config_path.exists() {
49 return Ok(true);
50 }
51
52 crate::fs::atomic_write(&config_path, b"[dependencies]\n")?;
53 Ok(false)
54}
55
56pub fn run(args: &InitArgs, explicit_root: Option<&Path>, json: bool) -> Result<i32, MarsError> {
58 let project_root = explicit_root.map(Path::to_path_buf).unwrap_or_else(|| {
60 super::default_project_root().unwrap_or_else(|_| std::env::current_dir().unwrap())
61 });
62
63 let target = if let Some(t) = args.target.as_deref() {
65 t.to_string()
66 } else {
67 match crate::config::load(&project_root) {
69 Ok(config) => config
70 .settings
71 .managed_root
72 .unwrap_or_else(|| ".agents".into()),
73 Err(_) => ".agents".into(),
74 }
75 };
76
77 validate_target(&target)?;
78 let managed_root = project_root.join(&target);
79
80 std::fs::create_dir_all(&managed_root)?;
82 std::fs::create_dir_all(project_root.join(".mars"))?;
83
84 let already_initialized = ensure_consumer_config(&project_root)?;
85
86 persist_managed_root(&project_root, &target)?;
88
89 if !json {
90 if already_initialized {
91 output::print_info(&format!("{} already initialized", project_root.display()));
92 } else {
93 output::print_success(&format!(
94 "initialized {} with mars.toml",
95 project_root.display()
96 ));
97 }
98 }
99
100 if !args.link.is_empty() {
102 let ctx = super::MarsContext::from_roots(project_root.clone(), managed_root.clone())?;
103 for link_target in &args.link {
104 let link_args = super::link::LinkArgs {
105 target: link_target.clone(),
106 unlink: false,
107 };
108 super::link::run(&link_args, &ctx, json)?;
109 }
110 }
111
112 if json {
113 output::print_json(&serde_json::json!({
114 "ok": true,
115 "project_root": project_root.to_string_lossy(),
116 "managed_root": managed_root.to_string_lossy(),
117 "already_initialized": already_initialized,
118 "links": args.link,
119 }));
120 }
121
122 Ok(0)
123}
124
125fn persist_managed_root(project_root: &Path, target: &str) -> Result<(), MarsError> {
127 match crate::config::load(project_root) {
128 Ok(mut config) => {
129 config.settings.managed_root = if target == ".agents" {
130 None
131 } else {
132 Some(target.to_string())
133 };
134 crate::config::save(project_root, &config)?;
135 }
136 Err(MarsError::Config(ConfigError::NotFound { .. })) => {
137 }
139 Err(e) => return Err(e),
140 }
141 Ok(())
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use tempfile::TempDir;
148
149 #[test]
150 fn validate_target_accepts_simple_names() {
151 assert!(validate_target(".agents").is_ok());
152 assert!(validate_target(".claude").is_ok());
153 assert!(validate_target("my-agents").is_ok());
154 }
155
156 #[test]
157 fn validate_target_rejects_paths() {
158 assert!(validate_target("./foo").is_err());
159 assert!(validate_target("foo/bar").is_err());
160 assert!(validate_target("/absolute/path").is_err());
161 }
162
163 #[test]
164 fn validate_target_rejects_dots() {
165 assert!(validate_target(".").is_err());
166 assert!(validate_target("..").is_err());
167 }
168
169 #[test]
170 fn validate_target_rejects_empty() {
171 assert!(validate_target("").is_err());
172 }
173
174 #[test]
175 fn ensure_consumer_config_creates_root_mars_toml() {
176 let dir = TempDir::new().unwrap();
177
178 let already = ensure_consumer_config(dir.path()).unwrap();
179 assert!(!already);
180
181 let content = std::fs::read_to_string(dir.path().join("mars.toml")).unwrap();
182 assert!(content.contains("[dependencies]"));
183 }
184
185 #[test]
186 fn ensure_consumer_config_accepts_existing_mars_toml() {
187 let dir = TempDir::new().unwrap();
188 std::fs::write(
189 dir.path().join("mars.toml"),
190 "[package]\nname = \"pkg\"\nversion = \"0.1.0\"\n",
191 )
192 .unwrap();
193
194 let already = ensure_consumer_config(dir.path()).unwrap();
195 assert!(already);
196 }
197}