algocline_app/service/
init.rs1use std::path::Path;
4
5use super::alc_toml::{alc_toml_path, save_alc_toml};
6use super::project::resolve_project_root;
7use super::AppService;
8
9const GITIGNORE_ENTRIES: &[&str] = &["alc.local.toml", ".alc-install.lock"];
21
22impl AppService {
23 pub async fn init(&self, project_root: Option<String>) -> Result<String, String> {
24 let root = match resolve_project_root(project_root.as_deref()) {
26 Some(r) => r,
27 None => std::env::current_dir().map_err(|e| format!("Cannot determine cwd: {e}"))?,
28 };
29
30 let path = alc_toml_path(&root);
31 if path.exists() {
32 return Err(format!("alc.toml already exists at {}", path.display()));
33 }
34
35 let doc: toml_edit::DocumentMut = "[packages]\n"
36 .parse()
37 .map_err(|e: toml_edit::TomlError| format!("Internal error: {e}"))?;
38 save_alc_toml(&root, &doc)?;
39
40 let gitignore_path = root.join(".gitignore");
49 let mut gitignore_updated = false;
50 for entry in GITIGNORE_ENTRIES {
51 if update_gitignore(&root, entry)? {
52 gitignore_updated = true;
53 }
54 }
55
56 let result = serde_json::json!({
57 "created": path.display().to_string(),
58 "gitignore_path": gitignore_path.display().to_string(),
59 "gitignore_updated": gitignore_updated,
60 });
61 Ok(result.to_string())
62 }
63}
64
65pub(crate) fn update_gitignore(root: &Path, entry: &str) -> Result<bool, String> {
77 let path = root.join(".gitignore");
78
79 if !path.exists() {
80 std::fs::write(&path, format!("{entry}\n"))
81 .map_err(|e| format!("Failed to create {}: {e}", path.display()))?;
82 return Ok(true);
83 }
84
85 let existing = std::fs::read_to_string(&path)
86 .map_err(|e| format!("Failed to read {}: {e}", path.display()))?;
87
88 let already_present = existing.lines().any(|line| {
89 let trimmed = line.trim();
90 !trimmed.starts_with('#') && trimmed == entry
91 });
92
93 if already_present {
94 return Ok(false);
95 }
96
97 let mut new_content = existing;
98 if !new_content.is_empty() && !new_content.ends_with('\n') {
99 new_content.push('\n');
100 }
101 new_content.push_str(entry);
102 new_content.push('\n');
103
104 std::fs::write(&path, new_content)
105 .map_err(|e| format!("Failed to write {}: {e}", path.display()))?;
106 Ok(true)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::update_gitignore;
112 use crate::service::test_support::make_app_service as make_service;
113
114 #[tokio::test]
115 async fn init_creates_alc_toml() {
116 let tmp = tempfile::tempdir().unwrap();
117 let svc = make_service().await;
118 let result = svc
119 .init(Some(tmp.path().to_str().unwrap().to_string()))
120 .await
121 .unwrap();
122 assert!(result.contains("created"));
123 assert!(tmp.path().join("alc.toml").exists());
124
125 let content = std::fs::read_to_string(tmp.path().join("alc.toml")).unwrap();
126 assert!(content.contains("[packages]"));
127 }
128
129 #[tokio::test]
130 async fn init_fails_if_alc_toml_exists() {
131 let tmp = tempfile::tempdir().unwrap();
132 std::fs::write(tmp.path().join("alc.toml"), "[packages]\n").unwrap();
133 let svc = make_service().await;
134 let err = svc
135 .init(Some(tmp.path().to_str().unwrap().to_string()))
136 .await
137 .unwrap_err();
138 assert!(err.contains("already exists"));
139 }
140
141 #[tokio::test]
142 async fn init_creates_gitignore_when_absent() {
143 let tmp = tempfile::tempdir().unwrap();
144 let svc = make_service().await;
145 let raw = svc
146 .init(Some(tmp.path().to_str().unwrap().to_string()))
147 .await
148 .unwrap();
149
150 let json: serde_json::Value = serde_json::from_str(&raw).unwrap();
151 assert_eq!(json["gitignore_updated"], true);
152
153 let gi = tmp.path().join(".gitignore");
154 assert!(gi.exists());
155 let content = std::fs::read_to_string(&gi).unwrap();
156 assert_eq!(content, "alc.local.toml\n.alc-install.lock\n");
157 }
158
159 #[tokio::test]
160 async fn init_appends_to_existing_gitignore() {
161 let tmp = tempfile::tempdir().unwrap();
162 let gi = tmp.path().join(".gitignore");
163 std::fs::write(&gi, "target\nworkspace\n").unwrap();
164
165 let svc = make_service().await;
166 let raw = svc
167 .init(Some(tmp.path().to_str().unwrap().to_string()))
168 .await
169 .unwrap();
170 let json: serde_json::Value = serde_json::from_str(&raw).unwrap();
171 assert_eq!(json["gitignore_updated"], true);
172
173 let content = std::fs::read_to_string(&gi).unwrap();
174 assert_eq!(
175 content,
176 "target\nworkspace\nalc.local.toml\n.alc-install.lock\n"
177 );
178 }
179
180 #[tokio::test]
181 async fn init_is_idempotent_on_gitignore_entries() {
182 let tmp = tempfile::tempdir().unwrap();
183 let gi = tmp.path().join(".gitignore");
184 std::fs::write(
185 &gi,
186 "target\nalc.local.toml\n.alc-install.lock\nworkspace\n",
187 )
188 .unwrap();
189
190 let svc = make_service().await;
191 let raw = svc
192 .init(Some(tmp.path().to_str().unwrap().to_string()))
193 .await
194 .unwrap();
195 let json: serde_json::Value = serde_json::from_str(&raw).unwrap();
196 assert_eq!(json["gitignore_updated"], false);
197
198 let content = std::fs::read_to_string(&gi).unwrap();
200 assert_eq!(
201 content,
202 "target\nalc.local.toml\n.alc-install.lock\nworkspace\n"
203 );
204 }
205
206 #[tokio::test]
207 async fn init_partial_existing_gitignore_updates_missing_entry_only() {
208 let tmp = tempfile::tempdir().unwrap();
211 let gi = tmp.path().join(".gitignore");
212 std::fs::write(&gi, "target\nalc.local.toml\n").unwrap();
213
214 let svc = make_service().await;
215 let raw = svc
216 .init(Some(tmp.path().to_str().unwrap().to_string()))
217 .await
218 .unwrap();
219 let json: serde_json::Value = serde_json::from_str(&raw).unwrap();
220 assert_eq!(json["gitignore_updated"], true);
221
222 let content = std::fs::read_to_string(&gi).unwrap();
223 assert_eq!(content, "target\nalc.local.toml\n.alc-install.lock\n");
224 }
225
226 #[tokio::test]
227 async fn update_gitignore_adds_trailing_newline_if_missing() {
228 let tmp = tempfile::tempdir().unwrap();
229 let gi = tmp.path().join(".gitignore");
230 std::fs::write(&gi, "target").unwrap(); let updated = update_gitignore(tmp.path(), "alc.local.toml").unwrap();
233 assert!(updated);
234
235 let content = std::fs::read_to_string(&gi).unwrap();
236 assert_eq!(content, "target\nalc.local.toml\n");
237 }
238
239 #[tokio::test]
240 async fn update_gitignore_does_not_match_commented_line() {
241 let tmp = tempfile::tempdir().unwrap();
244 let gi = tmp.path().join(".gitignore");
245 std::fs::write(&gi, "# alc.local.toml\ntarget\n").unwrap();
246
247 let updated = update_gitignore(tmp.path(), "alc.local.toml").unwrap();
248 assert!(updated);
249
250 let content = std::fs::read_to_string(&gi).unwrap();
251 assert_eq!(content, "# alc.local.toml\ntarget\nalc.local.toml\n");
252 }
253}