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