1use crate::config::{global_savecontext_dir, is_test_mode};
15use crate::error::{Error, Result};
16use crate::sync::gitignore_content;
17use serde::Serialize;
18use std::fs;
19use std::path::{Path, PathBuf};
20
21#[derive(Serialize)]
22struct InitOutput {
23 path: PathBuf,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 database: Option<PathBuf>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 export_dir: Option<PathBuf>,
28}
29
30pub fn execute(global: bool, force: bool, json: bool) -> Result<()> {
39 if global {
40 execute_global_init(force, json)
41 } else {
42 execute_project_init(force, json)
43 }
44}
45
46fn execute_global_init(force: bool, json: bool) -> Result<()> {
52 let base_dir = global_savecontext_dir().ok_or_else(|| {
53 Error::Config("Could not determine global SaveContext directory".to_string())
54 })?;
55
56 let data_dir = if is_test_mode() {
58 base_dir.join("test")
59 } else {
60 base_dir.join("data")
61 };
62
63 let db_path = data_dir.join("savecontext.db");
65 if db_path.exists() && !force {
66 return Err(Error::AlreadyInitialized { path: db_path });
67 }
68
69 fs::create_dir_all(&data_dir)?;
71
72 if !db_path.exists() || force {
74 fs::File::create(&db_path)?;
75 }
76
77 let gitignore_path = base_dir.join(".gitignore");
79 if !gitignore_path.exists() || force {
80 let gitignore = "# Everything in global SaveContext is local-only\n*\n";
81 fs::write(&gitignore_path, gitignore)?;
82 }
83
84 if json {
85 let output = InitOutput {
86 path: base_dir,
87 database: Some(db_path),
88 export_dir: None,
89 };
90 let payload = serde_json::to_string(&output)?;
91 println!("{payload}");
92 } else {
93 println!("Initialized global SaveContext database");
94 println!(" Database: {}", db_path.display());
95 println!();
96 println!("Next: Run 'sc init' in your project directories to set up JSONL sync.");
97 }
98
99 Ok(())
100}
101
102fn execute_project_init(force: bool, json: bool) -> Result<()> {
107 let base_dir = Path::new(".").join(".savecontext");
108
109 if base_dir.exists() && !force {
111 return Err(Error::AlreadyInitialized { path: base_dir });
112 }
113
114 fs::create_dir_all(&base_dir)?;
116
117 let gitignore_path = base_dir.join(".gitignore");
119 if !gitignore_path.exists() || force {
120 fs::write(&gitignore_path, gitignore_content())?;
121 }
122
123 let config_path = base_dir.join("config.json");
125 if !config_path.exists() {
126 let config = r#"{
127 "default_priority": 2,
128 "default_type": "task"
129}
130"#;
131 fs::write(&config_path, config)?;
132 }
133
134 let db_subdir = if is_test_mode() { "test" } else { "data" };
136 let global_db = global_savecontext_dir()
137 .map(|dir| dir.join(db_subdir).join("savecontext.db"))
138 .filter(|p| p.exists());
139
140 if json {
141 let output = InitOutput {
142 path: base_dir,
143 database: global_db.clone(),
144 export_dir: Some(PathBuf::from(".savecontext")),
145 };
146 let payload = serde_json::to_string(&output)?;
147 println!("{payload}");
148 } else {
149 println!(
150 "Initialized SaveContext project in {}",
151 std::env::current_dir()
152 .map(|p| p.display().to_string())
153 .unwrap_or_else(|_| ".".to_string())
154 );
155 println!(" Export directory: .savecontext/");
156
157 if let Some(db) = global_db {
158 println!(" Database: {}", db.display());
159 } else {
160 println!();
161 println!(
162 "⚠️ Global database not found. Run 'sc init --global' first to create it."
163 );
164 }
165 }
166
167 Ok(())
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use std::sync::Mutex;
174 use tempfile::TempDir;
175
176 static CWD_LOCK: Mutex<()> = Mutex::new(());
178
179 fn with_temp_cwd<F, R>(f: F) -> R
180 where
181 F: FnOnce(&Path) -> R,
182 {
183 let _lock = CWD_LOCK.lock().unwrap();
184 let original_cwd = std::env::current_dir().unwrap();
185 let temp_dir = TempDir::new().unwrap();
186 std::env::set_current_dir(temp_dir.path()).unwrap();
187
188 let result = f(temp_dir.path());
189
190 std::env::set_current_dir(original_cwd).unwrap();
191 result
192 }
193
194 #[test]
195 fn test_project_init_creates_export_directory() {
196 with_temp_cwd(|temp_path| {
197 let result = execute(false, false, false);
198 assert!(result.is_ok());
199
200 assert!(temp_path.join(".savecontext").exists());
202 assert!(temp_path.join(".savecontext/.gitignore").exists());
203 assert!(temp_path.join(".savecontext/config.json").exists());
204
205 assert!(!temp_path.join(".savecontext/data").exists());
207 assert!(!temp_path.join(".savecontext/data/savecontext.db").exists());
208 });
209 }
210
211 #[test]
212 fn test_project_init_fails_if_already_initialized() {
213 with_temp_cwd(|_| {
214 assert!(execute(false, false, false).is_ok());
216
217 let result = execute(false, false, false);
219 assert!(matches!(result, Err(Error::AlreadyInitialized { .. })));
220 });
221 }
222
223 #[test]
224 fn test_project_init_force_overwrites() {
225 with_temp_cwd(|_| {
226 assert!(execute(false, false, false).is_ok());
227 assert!(execute(false, true, false).is_ok()); });
229 }
230
231 }