1use std::path::{Path, PathBuf};
12
13use crate::error::{ConfigError, MarsError};
14
15use super::output;
16
17#[derive(Debug, clap::Args)]
19pub struct InitArgs {
20 pub target: Option<String>,
22
23 #[arg(long, value_name = "DIR")]
25 pub link: Vec<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub(super) struct InitializedProject {
30 pub project_root: PathBuf,
31 pub managed_root: Option<PathBuf>,
32 pub already_initialized: bool,
33}
34
35fn ensure_consumer_config(project_root: &Path) -> Result<bool, MarsError> {
36 let config_path = project_root.join("mars.toml");
37 if config_path.exists() {
38 return Ok(true);
39 }
40
41 crate::fs::atomic_write(&config_path, b"[dependencies]\n")?;
42 Ok(false)
43}
44
45pub(super) fn initialize_project(
46 explicit_root: Option<&Path>,
47 target_override: Option<&str>,
48) -> Result<InitializedProject, MarsError> {
49 let project_root = explicit_root
50 .map(Path::to_path_buf)
51 .unwrap_or_else(|| std::env::current_dir().expect("cannot determine current directory"));
52
53 std::fs::create_dir_all(project_root.join(".mars"))?;
54
55 let already_initialized = ensure_consumer_config(&project_root)?;
56
57 let managed_root = if let Some(target) = explicit_init_target(&project_root, target_override)? {
58 super::target::validate_target(&target)?;
59 let managed_root = project_root.join(&target);
60 std::fs::create_dir_all(&managed_root)?;
61 persist_managed_root(&project_root, Some(&target))?;
62 Some(managed_root)
63 } else {
64 persist_managed_root(&project_root, None)?;
65 None
66 };
67
68 Ok(InitializedProject {
69 project_root,
70 managed_root,
71 already_initialized,
72 })
73}
74
75pub fn run(args: &InitArgs, explicit_root: Option<&Path>, json: bool) -> Result<i32, MarsError> {
79 let initialized = initialize_project(explicit_root, args.target.as_deref())?;
80 let project_root = initialized.project_root;
81 let managed_root = initialized.managed_root;
82 let already_initialized = initialized.already_initialized;
83
84 if !json {
85 if already_initialized {
86 output::print_info(&format!("{} already initialized", project_root.display()));
87 } else {
88 output::print_success(&format!(
89 "initialized {} with mars.toml",
90 project_root.display()
91 ));
92 }
93 }
94
95 if !args.link.is_empty() {
97 let context_managed_root = managed_root
98 .clone()
99 .unwrap_or_else(|| project_root.join(".mars"));
100 let ctx = super::MarsContext::from_roots(project_root.clone(), context_managed_root)?;
101 for link_target in &args.link {
102 let link_args = super::link::LinkArgs {
103 target: link_target.clone(),
104 };
105 super::link::run(&link_args, &ctx, json)?;
106 }
107 }
108
109 if json {
110 output::print_json(&serde_json::json!({
111 "ok": true,
112 "project_root": project_root.to_string_lossy(),
113 "managed_root": managed_root.as_ref().map(|path| path.to_string_lossy().to_string()),
114 "already_initialized": already_initialized,
115 "links": args.link,
116 }));
117 }
118
119 Ok(0)
120}
121
122fn explicit_init_target(
123 project_root: &Path,
124 target_override: Option<&str>,
125) -> Result<Option<String>, MarsError> {
126 if let Some(target) = target_override {
127 return Ok(Some(target.to_string()));
128 }
129
130 match crate::config::load(project_root) {
131 Ok(config) => Ok(config.settings.managed_root),
132 Err(MarsError::Config(ConfigError::NotFound { .. })) => Ok(None),
133 Err(e) => Err(e),
134 }
135}
136
137fn persist_managed_root(project_root: &Path, target: Option<&str>) -> Result<(), MarsError> {
139 match crate::config::load(project_root) {
140 Ok(mut config) => {
141 config.settings.managed_root = target.map(str::to_string);
142 crate::config::save(project_root, &config)?;
143 }
144 Err(MarsError::Config(ConfigError::NotFound { .. })) => {
145 }
147 Err(e) => return Err(e),
148 }
149 Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::cli::target::validate_target;
156 use tempfile::TempDir;
157
158 #[test]
159 fn validate_target_accepts_simple_names() {
160 assert!(validate_target(".agents").is_ok());
161 assert!(validate_target(".claude").is_ok());
162 assert!(validate_target("my-agents").is_ok());
163 }
164
165 #[test]
166 fn validate_target_rejects_paths() {
167 assert!(validate_target("./foo").is_err());
168 assert!(validate_target("foo/bar").is_err());
169 assert!(validate_target("/absolute/path").is_err());
170 }
171
172 #[test]
173 fn validate_target_rejects_dots() {
174 assert!(validate_target(".").is_err());
175 assert!(validate_target("..").is_err());
176 }
177
178 #[test]
179 fn validate_target_rejects_empty() {
180 assert!(validate_target("").is_err());
181 }
182
183 #[test]
184 fn ensure_consumer_config_creates_root_mars_toml() {
185 let dir = TempDir::new().unwrap();
186
187 let already = ensure_consumer_config(dir.path()).unwrap();
188 assert!(!already);
189
190 let content = std::fs::read_to_string(dir.path().join("mars.toml")).unwrap();
191 assert!(content.contains("[dependencies]"));
192 }
193
194 #[test]
195 fn ensure_consumer_config_accepts_existing_mars_toml() {
196 let dir = TempDir::new().unwrap();
197 std::fs::write(
198 dir.path().join("mars.toml"),
199 "[package]\nname = \"pkg\"\nversion = \"0.1.0\"\n",
200 )
201 .unwrap();
202
203 let already = ensure_consumer_config(dir.path()).unwrap();
204 assert!(already);
205 }
206
207 #[test]
208 fn initialize_project_without_target_creates_mars_only() {
209 let dir = TempDir::new().unwrap();
210
211 let initialized = initialize_project(Some(dir.path()), None).unwrap();
212
213 assert!(dir.path().join(".mars").exists());
214 assert!(!dir.path().join(".agents").exists());
215 assert!(initialized.managed_root.is_none());
216
217 let config = crate::config::load(dir.path()).unwrap();
218 assert!(config.settings.managed_root.is_none());
219 }
220
221 #[test]
222 fn initialize_project_with_explicit_target_persists_managed_root() {
223 let dir = TempDir::new().unwrap();
224
225 let initialized = initialize_project(Some(dir.path()), Some(".claude")).unwrap();
226
227 assert!(dir.path().join(".mars").exists());
228 assert!(dir.path().join(".claude").exists());
229 assert_eq!(initialized.managed_root, Some(dir.path().join(".claude")));
230
231 let config = crate::config::load(dir.path()).unwrap();
232 assert_eq!(config.settings.managed_root.as_deref(), Some(".claude"));
233 }
234
235 #[test]
236 fn initialize_project_preserves_existing_managed_root_when_no_target_given() {
237 let dir = TempDir::new().unwrap();
238 std::fs::write(
239 dir.path().join("mars.toml"),
240 "[settings]\nmanaged_root = \".claude\"\n",
241 )
242 .unwrap();
243
244 let initialized = initialize_project(Some(dir.path()), None).unwrap();
245
246 assert!(dir.path().join(".claude").exists());
247 assert_eq!(initialized.managed_root, Some(dir.path().join(".claude")));
248 }
249
250 #[test]
251 fn initialize_project_with_explicit_agents_persists_deprecated_target() {
252 let dir = TempDir::new().unwrap();
253
254 let initialized = initialize_project(Some(dir.path()), Some(".agents")).unwrap();
255
256 assert!(dir.path().join(".agents").exists());
257 assert_eq!(initialized.managed_root, Some(dir.path().join(".agents")));
258
259 let config = crate::config::load(dir.path()).unwrap();
260 assert_eq!(config.settings.managed_root.as_deref(), Some(".agents"));
261 }
262}