1use anyhow::{Context, Result};
2use colored::*;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7use crate::log::Log;
8use super::license;
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct ProjectConfig {
11 pub project: ProjectSettings,
12 pub shortcuts: HashMap<String, String>,
13 pub auto_fix: AutoFixSettings,
14 pub journey: JourneySettings,
15 pub build: BuildSettings,
16 pub version_control: VersionControlSettings,
17 pub hooks: HookSettings,
18}
19#[derive(Debug, Serialize, Deserialize, Clone)]
20pub struct ProjectSettings {
21 pub name: Option<String>,
22 pub default_journey: Option<String>,
23 pub theme: String,
24 pub auto_checklist: bool,
25 pub track_performance: bool,
26}
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct AutoFixSettings {
29 pub format_on_save: bool,
30 pub clippy_on_build: bool,
31 pub auto_deps_update: bool,
32 pub fix_warnings: bool,
33 pub suggest_fixes: bool,
34}
35#[derive(Debug, Serialize, Deserialize, Clone)]
36pub struct JourneySettings {
37 pub auto_record: bool,
38 pub share_templates: bool,
39 pub max_recordings: usize,
40 pub interactive_by_default: bool,
41}
42#[derive(Debug, Serialize, Deserialize, Clone)]
43pub struct BuildSettings {
44 pub default_profile: String,
45 pub parallel_jobs: Option<usize>,
46 pub target_dir: Option<PathBuf>,
47 pub incremental: bool,
48 pub cache_artifacts: bool,
49}
50#[derive(Debug, Serialize, Deserialize, Clone)]
51pub struct HookSettings {
52 pub pre_build: Vec<String>,
53 pub post_build: Vec<String>,
54 pub on_error: Vec<String>,
55 pub on_success: Vec<String>,
56}
57
58#[derive(Debug, Serialize, Deserialize, Clone)]
59pub struct VersionControlSettings {
60 pub auto_git_commit: bool,
61 pub auto_anchor_git: bool,
62}
63pub struct ConfigManager {
64 global_config: ProjectConfig,
65 local_config: Option<ProjectConfig>,
66 global_path: PathBuf,
67 local_path: PathBuf,
68}
69impl ConfigManager {
70 pub fn new() -> Result<Self> {
71 let global_path = dirs::home_dir()
72 .context("Could not find home directory")?
73 .join(".shipwreck")
74 .join("config.toml");
75 let local_path = PathBuf::from(".cg");
76 let global_config = if global_path.exists() {
77 let content = fs::read_to_string(&global_path)?;
78 toml::from_str(&content)?
79 } else {
80 ProjectConfig::default()
81 };
82 let local_config = if local_path.exists() {
83 let content = fs::read_to_string(&local_path)?;
84 Some(toml::from_str(&content)?)
85 } else {
86 None
87 };
88 Ok(Self {
89 global_config,
90 local_config,
91 global_path,
92 local_path,
93 })
94 }
95 pub fn get(&self, key: &str) -> Option<String> {
96 let parts: Vec<&str> = key.split('.').collect();
97 if let Some(ref local) = self.local_config {
98 if let Some(value) = self.get_from_config(local, &parts) {
99 return Some(value);
100 }
101 }
102 self.get_from_config(&self.global_config, &parts)
103 }
104 fn get_from_config(&self, config: &ProjectConfig, parts: &[&str]) -> Option<String> {
105 match parts {
106 ["project", field] => {
107 match *field {
108 "name" => config.project.name.clone(),
109 "default_journey" => config.project.default_journey.clone(),
110 "theme" => Some(config.project.theme.clone()),
111 "auto_checklist" => Some(config.project.auto_checklist.to_string()),
112 "track_performance" => {
113 Some(config.project.track_performance.to_string())
114 }
115 _ => None,
116 }
117 }
118 ["shortcuts", key] => config.shortcuts.get(*key).cloned(),
119 ["auto_fix", field] => {
120 match *field {
121 "format_on_save" => Some(config.auto_fix.format_on_save.to_string()),
122 "clippy_on_build" => {
123 Some(config.auto_fix.clippy_on_build.to_string())
124 }
125 "auto_deps_update" => {
126 Some(config.auto_fix.auto_deps_update.to_string())
127 }
128 "fix_warnings" => Some(config.auto_fix.fix_warnings.to_string()),
129 "suggest_fixes" => Some(config.auto_fix.suggest_fixes.to_string()),
130 _ => None,
131 }
132 }
133 ["build", field] => {
134 match *field {
135 "default_profile" => Some(config.build.default_profile.clone()),
136 "parallel_jobs" => config.build.parallel_jobs.map(|j| j.to_string()),
137 "incremental" => Some(config.build.incremental.to_string()),
138 "cache_artifacts" => Some(config.build.cache_artifacts.to_string()),
139 _ => None,
140 }
141 }
142 _ => None,
143 }
144 }
145 pub fn set(&mut self, key: &str, value: &str, local: bool) -> Result<()> {
146 let config = if local {
147 self.local_config.get_or_insert_with(ProjectConfig::default)
148 } else {
149 &mut self.global_config
150 };
151 let parts: Vec<&str> = key.split('.').collect();
152 match parts.as_slice() {
153 ["project", field] => {
154 match *field {
155 "name" => config.project.name = Some(value.to_string()),
156 "default_journey" => {
157 config.project.default_journey = Some(value.to_string());
158 }
159 "theme" => config.project.theme = value.to_string(),
160 "auto_checklist" => config.project.auto_checklist = value.parse()?,
161 "track_performance" => {
162 config.project.track_performance = value.parse()?;
163 }
164 _ => return Err(anyhow::anyhow!("Unknown project field: {}", field)),
165 }
166 }
167 ["shortcuts", key] => {
168 config.shortcuts.insert(key.to_string(), value.to_string());
169 }
170 ["auto_fix", field] => {
171 match *field {
172 "format_on_save" => config.auto_fix.format_on_save = value.parse()?,
173 "clippy_on_build" => config.auto_fix.clippy_on_build = value.parse()?,
174 "auto_deps_update" => {
175 config.auto_fix.auto_deps_update = value.parse()?;
176 }
177 "fix_warnings" => config.auto_fix.fix_warnings = value.parse()?,
178 "suggest_fixes" => config.auto_fix.suggest_fixes = value.parse()?,
179 _ => return Err(anyhow::anyhow!("Unknown auto_fix field: {}", field)),
180 }
181 }
182 ["build", field] => {
183 match *field {
184 "default_profile" => config.build.default_profile = value.to_string(),
185 "parallel_jobs" => config.build.parallel_jobs = Some(value.parse()?),
186 "incremental" => config.build.incremental = value.parse()?,
187 "cache_artifacts" => config.build.cache_artifacts = value.parse()?,
188 _ => return Err(anyhow::anyhow!("Unknown build field: {}", field)),
189 }
190 }
191 _ => return Err(anyhow::anyhow!("Unknown config key: {}", key)),
192 }
193 self.save(local)?;
194 println!("✅ Config set: {} = {}", key.cyan(), value.green());
195 Ok(())
196 }
197 pub fn add_shortcut(
198 &mut self,
199 name: &str,
200 command: &str,
201 local: bool,
202 ) -> Result<()> {
203 let config = if local {
204 self.local_config.get_or_insert_with(ProjectConfig::default)
205 } else {
206 &mut self.global_config
207 };
208 config.shortcuts.insert(name.to_string(), command.to_string());
209 self.save(local)?;
210 println!("✅ Shortcut added: {} → {}", name.cyan(), command.green());
211 Ok(())
212 }
213 pub fn get_shortcut(&self, name: &str) -> Option<String> {
214 if let Some(ref local) = self.local_config {
215 if let Some(cmd) = local.shortcuts.get(name) {
216 return Some(cmd.clone());
217 }
218 }
219 self.global_config.shortcuts.get(name).cloned()
220 }
221 pub fn list_shortcuts(&self) {
222 println!("{}", "=== Shortcuts ===".blue().bold());
223 let mut all_shortcuts = self.global_config.shortcuts.clone();
224 if let Some(ref local) = self.local_config {
225 for (name, cmd) in &local.shortcuts {
226 all_shortcuts.insert(name.clone(), cmd.clone());
227 }
228 }
229 if all_shortcuts.is_empty() {
230 println!("No shortcuts defined");
231 } else {
232 for (name, cmd) in all_shortcuts {
233 println!(" {} → {}", name.cyan(), cmd.green());
234 }
235 }
236 }
237 pub fn add_hook(
238 &mut self,
239 hook_type: &str,
240 command: &str,
241 local: bool,
242 ) -> Result<()> {
243 let config = if local {
244 self.local_config.get_or_insert_with(ProjectConfig::default)
245 } else {
246 &mut self.global_config
247 };
248 match hook_type {
249 "pre_build" => config.hooks.pre_build.push(command.to_string()),
250 "post_build" => config.hooks.post_build.push(command.to_string()),
251 "on_error" => config.hooks.on_error.push(command.to_string()),
252 "on_success" => config.hooks.on_success.push(command.to_string()),
253 _ => return Err(anyhow::anyhow!("Unknown hook type: {}", hook_type)),
254 }
255 self.save(local)?;
256 println!("✅ Hook added: {} → {}", hook_type.cyan(), command.green());
257 Ok(())
258 }
259 pub fn run_hooks(&self, hook_type: &str) -> Result<()> {
260 let mut hooks = Vec::new();
261 match hook_type {
262 "pre_build" => {
263 hooks.extend(self.global_config.hooks.pre_build.clone());
264 if let Some(ref local) = self.local_config {
265 hooks.extend(local.hooks.pre_build.clone());
266 }
267 }
268 "post_build" => {
269 hooks.extend(self.global_config.hooks.post_build.clone());
270 if let Some(ref local) = self.local_config {
271 hooks.extend(local.hooks.post_build.clone());
272 }
273 }
274 "on_error" => {
275 hooks.extend(self.global_config.hooks.on_error.clone());
276 if let Some(ref local) = self.local_config {
277 hooks.extend(local.hooks.on_error.clone());
278 }
279 }
280 "on_success" => {
281 hooks.extend(self.global_config.hooks.on_success.clone());
282 if let Some(ref local) = self.local_config {
283 hooks.extend(local.hooks.on_success.clone());
284 }
285 }
286 _ => {}
287 }
288 for hook in hooks {
289 println!("🎣 Running {} hook: {}", hook_type, hook.dimmed());
290 std::process::Command::new("sh").arg("-c").arg(&hook).status()?;
291 }
292 Ok(())
293 }
294 pub fn init_local(&mut self) -> Result<()> {
295 if self.local_path.exists() {
296 println!("⚠️ Local config already exists");
297 return Ok(());
298 }
299 let config = ProjectConfig::default();
300 let toml = toml::to_string_pretty(&config)?;
301 fs::write(&self.local_path, toml)?;
302 self.local_config = Some(config);
303 println!("✅ Created local config file: .cg");
304 println!(" Edit this file to customize project-specific settings");
305 Ok(())
306 }
307 pub fn show(&self) {
308 println!("{}", "=== Configuration ===".blue().bold());
309 if let Some(ref local) = self.local_config {
310 println!("\n📁 Local Config (.cg):");
311 self.display_config(local, " ");
312 }
313 println!("\n🌍 Global Config:");
314 self.display_config(&self.global_config, " ");
315 }
316 fn display_config(&self, config: &ProjectConfig, prefix: &str) {
317 println!("{}Project:", prefix);
318 if let Some(ref name) = config.project.name {
319 println!("{} name: {}", prefix, name.green());
320 }
321 if let Some(ref journey) = config.project.default_journey {
322 println!("{} default_journey: {}", prefix, journey.green());
323 }
324 println!("{} theme: {}", prefix, config.project.theme.green());
325 println!("{} auto_checklist: {}", prefix, config.project.auto_checklist);
326 if !config.shortcuts.is_empty() {
327 println!("{}Shortcuts:", prefix);
328 for (name, cmd) in &config.shortcuts {
329 println!("{} {} → {}", prefix, name.cyan(), cmd);
330 }
331 }
332 println!("{}Auto Fix:", prefix);
333 println!("{} format_on_save: {}", prefix, config.auto_fix.format_on_save);
334 println!("{} clippy_on_build: {}", prefix, config.auto_fix.clippy_on_build);
335 println!("{}Build:", prefix);
336 println!("{} default_profile: {}", prefix, config.build.default_profile);
337 println!("{} incremental: {}", prefix, config.build.incremental);
338 }
339 pub fn save(&self, local: bool) -> Result<()> {
340 if local {
341 if let Some(ref config) = self.local_config {
342 let toml = toml::to_string_pretty(config)?;
343 fs::write(&self.local_path, toml)?;
344 }
345 } else {
346 let toml = toml::to_string_pretty(&self.global_config)?;
347 fs::create_dir_all(self.global_path.parent().unwrap())?;
348 fs::write(&self.global_path, toml)?;
349 }
350 Ok(())
351 }
352 pub fn merge_with_env(&mut self) {
353 if let Ok(val) = std::env::var("CM_DEFAULT_PROFILE") {
354 self.global_config.build.default_profile = val;
355 }
356 if let Ok(val) = std::env::var("CM_PARALLEL_JOBS") {
357 if let Ok(jobs) = val.parse() {
358 self.global_config.build.parallel_jobs = Some(jobs);
359 }
360 }
361 if let Ok(val) = std::env::var("CM_AUTO_FIX") {
362 if let Ok(enabled) = val.parse() {
363 self.global_config.auto_fix.format_on_save = enabled;
364 self.global_config.auto_fix.clippy_on_build = enabled;
365 }
366 }
367 }
368 pub fn reset(&mut self) -> Result<()> {
369 self.local_config = Some(ProjectConfig::default());
370 self.save(true)?;
371 println!("✅ Local configuration reset to defaults");
372 Ok(())
373 }
374}
375impl Default for ProjectConfig {
376 fn default() -> Self {
377 Self {
378 project: ProjectSettings {
379 name: None,
380 default_journey: None,
381 theme: "nautical".to_string(),
382 auto_checklist: true,
383 track_performance: true,
384 },
385 shortcuts: HashMap::new(),
386 auto_fix: AutoFixSettings {
387 format_on_save: false,
388 clippy_on_build: false,
389 auto_deps_update: false,
390 fix_warnings: false,
391 suggest_fixes: true,
392 },
393 journey: JourneySettings {
394 auto_record: false,
395 share_templates: false,
396 max_recordings: 100,
397 interactive_by_default: true,
398 },
399 build: BuildSettings {
400 default_profile: "dev".to_string(),
401 parallel_jobs: None,
402 target_dir: None,
403 incremental: true,
404 cache_artifacts: true,
405 },
406 version_control: VersionControlSettings {
407 auto_git_commit: true,
408 auto_anchor_git: true,
409 },
410 hooks: HookSettings {
411 pre_build: Vec::new(),
412 post_build: Vec::new(),
413 on_error: Vec::new(),
414 on_success: Vec::new(),
415 },
416 }
417 }
418}
419pub fn check_captain_authority(command: &str) -> Result<bool> {
420 let log = Log::new();
421 log.log(
422 &format!(
423 "Captain's authority check for command '{}' - all hands on deck!", command
424 ),
425 vec!["captain".to_string(), "authority".to_string()],
426 )?;
427 let license_manager = license::LicenseManager::new()?;
428 match license_manager.enforce_license(command) {
429 Ok(_) => Ok(true),
430 Err(e) => {
431 if e.to_string().contains("limit") {
432 log.log(
433 "Command quota exceeded",
434 vec!["captain".to_string(), "authority".to_string()],
435 )?;
436 } else if e.to_string().contains("License not found") {
437 log.log(
438 "Unauthorized vessel! No captain's papers found!",
439 vec!["captain".to_string(), "authority".to_string()],
440 )?;
441 } else {
442 log.log(
443 "Mutiny alert! Authority check failed",
444 vec!["captain".to_string(), "authority".to_string()],
445 )?;
446 }
447 Ok(false)
448 }
449 }
450}