1use anyhow::{Context, Result};
2use reqwest::blocking;
3use serde::{Deserialize, Serialize};
4use std::env;
5use chrono::{DateTime, Utc};
6use std::fs;
7use std::path::PathBuf;
8use crate::license_guard::LicenseGuard;
9use crate::log::Log;
10#[cfg(unix)]
11use std::os::unix::fs::PermissionsExt;
12#[derive(Debug, Deserialize)]
13pub struct LicenseValidation {
14 pub valid: bool,
15 pub tier: String,
16 pub remaining: Option<i32>,
17 pub used: Option<i32>,
18 pub unlimited: Option<bool>,
19 pub expires_at: Option<String>,
20 pub error: Option<String>,
21}
22#[derive(Debug, Serialize)]
23pub struct LicenseRequest {
24 pub license_key: String,
25 pub command: String,
26 pub timestamp: DateTime<Utc>,
27 pub ip_address: Option<String>,
28 pub user_agent: Option<String>,
29}
30#[derive(Debug, Serialize)]
31pub struct LicenseCheckRequest {
32 pub license_key: String,
33 pub action: String,
34}
35pub struct LicenseManager {
36 api_base_url: String,
37}
38impl LicenseManager {
39 pub fn new() -> Result<Self> {
40 dotenvy::dotenv().ok();
41 let api_base_url = env::var("CARGO_MATE_API")
42 .unwrap_or_else(|_| "https://cargo.do/api".to_string());
43 Ok(Self { api_base_url })
44 }
45 pub fn register_license(&self, license_key: &str) -> Result<()> {
46 let log = Log::new();
47 log.log(
48 "Registering license",
49 vec!["license".to_string(), "register".to_string()],
50 )?;
51 log.log(
52 "Note: Licenses are now automatically linked to users when purchased.",
53 vec!["license".to_string(), "register".to_string()],
54 )?;
55 log.log(
56 "This command is for linking existing licenses to your current installation.",
57 vec!["license".to_string(), "register".to_string()],
58 )?;
59 if license_key.len() < 10 {
60 log.log(
61 "Invalid license format",
62 vec!["license".to_string(), "register".to_string()],
63 )?;
64 return Err(
65 anyhow::anyhow!("Invalid license format. Expected: <license_string>"),
66 );
67 }
68 let user_id = self.get_or_create_user_id()?;
69 log.log("User ID", vec!["license".to_string(), "register".to_string()])?;
70 let affiliate_code: Option<String> = None;
71 let client = blocking::Client::new();
72 let mut payload = serde_json::json!(
73 { "license_key" : license_key, "user_id" : user_id }
74 );
75 if let Some(afl_code) = affiliate_code {
76 payload["affiliate_code"] = serde_json::json!(afl_code);
77 }
78 let url = format!("{}/register-license", self.api_base_url);
79 log.log(
80 "Calling API endpoint",
81 vec!["license".to_string(), "register".to_string()],
82 )?;
83 let response = client
84 .post(&url)
85 .json(&payload)
86 .send()
87 .context("Failed to connect to license registration API")?;
88 if !response.status().is_success() {
89 let status = response.status();
90 let error_text = response
91 .text()
92 .unwrap_or_else(|_| "Unknown error".to_string());
93 log.log(
94 "Registration failed",
95 vec!["license".to_string(), "register".to_string()],
96 )?;
97 crate::wtf::display_api_failure_art();
98 std::process::exit(1);
99 }
100 let registration_result: serde_json::Value = response
101 .json()
102 .context("Failed to parse license registration response")?;
103 if registration_result["success"] != true {
104 let error_msg = registration_result["error"]
105 .as_str()
106 .unwrap_or("Unknown registration error");
107 return Err(anyhow::anyhow!("License registration failed: {}", error_msg));
108 }
109 let tier = registration_result["tier"].as_str().unwrap_or("FREE").to_string();
110 self.save_local_license(license_key, &tier)?;
111 log.log(
112 "License registered successfully",
113 vec!["license".to_string(), "register".to_string()],
114 )?;
115 log.log("Tier", vec!["license".to_string(), "register".to_string()])?;
116 log.log("User", vec!["license".to_string(), "register".to_string()])?;
117 let obfuscated_license = LicenseGuard::obfuscate_license(license_key);
118 log.log(
119 "Obfuscated license",
120 vec!["license".to_string(), "register".to_string()],
121 )?;
122 if let Err(e) = LicenseGuard::store_license(&obfuscated_license) {
123 log.log(
124 "Error storing license",
125 vec!["license".to_string(), "register".to_string()],
126 )?;
127 }
128 log.log("License stored", vec!["license".to_string(), "register".to_string()])?;
129 Ok(())
130 }
131 pub fn show_user_info(&self) -> Result<()> {
132 let log = Log::new();
133 let user_id = self.get_or_create_user_id()?;
134 log.log("User Information", vec!["license".to_string(), "show".to_string()])?;
135 log.log("User ID", vec!["license".to_string(), "show".to_string()])?;
136 let license_key = match LicenseGuard::retrieve_license() {
137 Ok(Some(license)) => license,
138 Ok(None) => {
139 log.log(
140 "License not found",
141 vec!["license".to_string(), "show".to_string()],
142 )?;
143 return Ok(());
144 }
145 Err(_) => {
146 log.log(
147 "Failed to retrieve license",
148 vec!["license".to_string(), "show".to_string()],
149 )?;
150 return Ok(());
151 }
152 };
153 log.log("License", vec!["license".to_string(), "show".to_string()])?;
154 match self.check_license_status() {
155 Ok(validation) => {
156 log.log("Tier", vec!["license".to_string(), "show".to_string()])?;
157 if let Some(expires_at) = validation.expires_at {
158 log.log("Expires", vec!["license".to_string(), "show".to_string()])?;
159 }
160 if validation.tier == "FREE" {
161 if let Some(remaining) = validation.remaining {
162 log.log(
163 "Remaining commands",
164 vec!["license".to_string(), "show".to_string()],
165 )?;
166 }
167 }
168 }
169 Err(e) => {
170 log.log(
171 "License status check failed",
172 vec!["license".to_string(), "show".to_string()],
173 )?;
174 }
175 }
176 Ok(())
177 }
178 pub fn check_license_status(&self) -> Result<LicenseValidation> {
179 let license_key = self.get_local_license()?;
180 let user_id = self.get_or_create_user_id()?;
181 let client = blocking::Client::new();
182 let response = client
183 .post(&format!("{}/licenses/validate", self.api_base_url))
184 .json(
185 &serde_json::json!(
186 { "license_key" : license_key, "user_id" : user_id, "action" :
187 "check_status" }
188 ),
189 )
190 .send()
191 .context("Failed to connect to license API")?;
192 if response.status().is_success() {
193 let validation: LicenseValidation = response
194 .json()
195 .context("Failed to parse license validation response")?;
196 Ok(validation)
197 } else {
198 Ok(LicenseValidation {
199 valid: false,
200 tier: "FREE".to_string(),
201 remaining: Some(0),
202 used: None,
203 unlimited: Some(false),
204 expires_at: None,
205 error: Some("License not found or inactive".to_string()),
206 })
207 }
208 }
209 pub fn record_usage(&self, command: &str) -> Result<()> {
210 let log = Log::new();
211 let license_key = self.get_local_license()?;
212 let user_id = self.get_or_create_user_id()?;
213 let client = blocking::Client::new();
214 let response = client
215 .post(&format!("{}/licenses/validate", self.api_base_url))
216 .json(
217 &serde_json::json!(
218 { "license_key" : license_key, "user_id" : user_id, "action" :
219 "use_command" }
220 ),
221 )
222 .timeout(std::time::Duration::from_secs(3))
223 .send();
224 match response {
225 Ok(resp) if resp.status().is_success() => Ok(()),
226 Ok(resp) => {
227 let status_code = resp.status();
228 log.log(
229 "Warning: Failed to record command usage (HTTP {})",
230 vec!["license".to_string(), "record_usage".to_string()],
231 )?;
232 log.log(
233 "Your command was executed but usage may not be properly tracked.",
234 vec!["license".to_string(), "record_usage".to_string()],
235 )?;
236 Ok(())
237 }
238 Err(e) => {
239 log.log(
240 "Warning: Could not connect to license server to record usage",
241 vec!["license".to_string(), "record_usage".to_string()],
242 )?;
243 log.log(
244 "Your command was executed but usage may not be properly tracked.",
245 vec!["license".to_string(), "record_usage".to_string()],
246 )?;
247 eprintln!(
248 "⚠️ Your command was executed but usage may not be properly tracked."
249 );
250 Ok(())
251 }
252 }
253 }
254 pub fn enforce_license(&self, command: &str) -> Result<()> {
255 let log = Log::new();
256 let user_id = self.get_or_create_user_id()?;
257 let license_key = match LicenseGuard::retrieve_license() {
258 Ok(key) => key,
259 Err(_) => {
260 if let Ok(Some(key)) = LicenseGuard::retrieve_license() {
261 let home_dir = dirs::home_dir().unwrap();
262 let config_dir = home_dir.join(".shipwreck");
263 let license_file = config_dir.join("license.key");
264 let _ = fs::write(&license_file, &key);
265 Some(key)
266 } else {
267 eprintln!(
268 "┌─────────────────────────────────────────────────────┐"
269 );
270 eprintln!(
271 "│ ❌ FATAL: No valid license found │"
272 );
273 eprintln!(
274 "├─────────────────────────────────────────────────────┤"
275 );
276 eprintln!(
277 "│ Cargo Mate requires a license to operate. │"
278 );
279 eprintln!(
280 "│ Auto-registration may have failed. │"
281 );
282 eprintln!(
283 "│ │"
284 );
285 eprintln!(
286 "│ Please reinstall to get your FREE license: │"
287 );
288 eprintln!(
289 "│ curl -sSL https://get.cargo.do/mate | bash │"
290 );
291 eprintln!(
292 "└─────────────────────────────────────────────────────┘"
293 );
294 std::process::exit(1);
295 }
296 }
297 };
298 let validation = match self.check_license_status() {
299 Ok(validation) => validation,
300 Err(_) => {
301 match LicenseGuard::get_stored_license_info() {
302 Ok(Some(_)) => {
303 LicenseValidation {
304 valid: true,
305 tier: "PRO".to_string(),
306 remaining: None,
307 used: None,
308 unlimited: Some(true),
309 expires_at: None,
310 error: None,
311 }
312 }
313 _ => {
314 log.log(
315 "No internet connection. FREE tier requires online verification.",
316 vec!["license".to_string(), "enforce_license".to_string()],
317 )?;
318 std::process::exit(1);
319 }
320 }
321 }
322 };
323 if !validation.valid {
324 if let Some(error) = validation.error {
325 log.log(
326 "License validation failed",
327 vec!["license".to_string(), "enforce_license".to_string()],
328 )?;
329 }
330 log.log(
331 "Your license key",
332 vec!["license".to_string(), "enforce_license".to_string()],
333 )?;
334 std::process::exit(1);
335 }
336 if validation.tier == "FREE" {
337 let api_remaining = validation.remaining.unwrap_or(0);
338 let local_count = self.get_local_command_count()?;
339 let effective_remaining = if api_remaining <= local_count {
340 api_remaining
341 } else {
342 10 - local_count
343 };
344 if effective_remaining <= 0 {
345 eprintln!(
346 "┌─────────────────────────────────────────────────────┐"
347 );
348 eprintln!("│ :( DAILY LIMIT EXCEEDED 0/10 commands ): │");
349 eprintln!(
350 "├─────────────────────────────────────────────────────┤"
351 );
352 eprintln!("│ FREE tier limit exhausted for today. │");
353 eprintln!("│ │");
354 eprintln!("│ Options: │");
355 eprintln!(
356 "│ • Wait until midnight UTC for reset │"
357 );
358 eprintln!(
359 "│ • Upgrade to PRO: https://cargo.do/pro │"
360 );
361 eprintln!(
362 "└─────────────────────────────────────────────────────┘"
363 );
364 std::process::exit(1);
365 }
366 }
367 self.record_usage(command)?;
368 if validation.tier == "FREE" {
369 match self.increment_local_command_count() {
370 Ok(_) => {}
371 Err(e) => {}
372 }
373 }
374 Ok(())
375 }
376 pub fn get_or_create_user_id(&self) -> Result<String> {
377 let log = Log::new();
378 let home_dir = dirs::home_dir().context("Could not find home directory")?;
379 let config_dir = home_dir.join(".shipwreck");
380 let user_id_file = config_dir.join("user.id");
381 if user_id_file.exists() {
382 let user_id = fs::read_to_string(&user_id_file)
383 .context("Failed to read user ID file")?
384 .trim()
385 .to_string();
386 Ok(user_id)
387 } else {
388 fs::create_dir_all(&config_dir)
389 .context("Failed to create config directory")?;
390 let user_id = format!(
391 "cm_{}_{}", Utc::now().timestamp(), std::process::id()
392 );
393 let client = blocking::Client::new();
394 let payload = serde_json::json!({ "user_id" : user_id.clone() });
395 match client
396 .post(&format!("{}/users/register", self.api_base_url))
397 .json(&payload)
398 .timeout(std::time::Duration::from_secs(5))
399 .send()
400 {
401 Ok(response) if response.status().is_success() => {
402 if let Ok(json) = response.json::<serde_json::Value>() {
403 fs::write(&user_id_file, &user_id)
404 .context("Failed to save user ID file")?;
405 if let Some(license_key) = json["license_key"].as_str() {
406 let license_file = config_dir.join("license.key");
407 fs::write(&license_file, license_key)
408 .context("Failed to save license file")?;
409 if let Some(tier) = json["license_tier"].as_str() {
410 let tier_file = config_dir.join("license.tier");
411 fs::write(&tier_file, tier)
412 .context("Failed to save tier file")?;
413 }
414 if let Err(e) = crate::license_guard::LicenseGuard::store_license(
415 license_key,
416 ) {
417 log.log(
418 "Warning: Could not store license in hidden locations",
419 vec!["license".to_string(), "register".to_string()],
420 )?;
421 }
422 log.log(
423 "User registered successfully",
424 vec!["license".to_string(), "register".to_string()],
425 )?;
426 log.log(
427 "User ID",
428 vec!["license".to_string(), "register".to_string()],
429 )?;
430 log.log(
431 "License",
432 vec!["license".to_string(), "register".to_string()],
433 )?;
434 log.log(
435 "Affiliate program info",
436 vec!["license".to_string(), "register".to_string()],
437 )?;
438 let result: Result<(), anyhow::Error> = Ok(());
439 if let Err(e) = result {
440 log.log(
441 "Warning: Could not show affiliate info",
442 vec!["license".to_string(), "register".to_string()],
443 )?;
444 }
445 if let Some(afl_code) = json["affiliate_code"].as_str() {
446 let afl_file = config_dir.join("affiliate_code");
447 let _ = fs::write(&afl_file, afl_code);
448 log.log(
449 "Affiliate Code",
450 vec!["license".to_string(), "register".to_string()],
451 )?;
452 }
453 }
454 }
455 }
456 _ => {
457 fs::write(&user_id_file, &user_id)
458 .context("Failed to save user ID file")?;
459 log.log(
460 "Could not connect to license server. Running in offline mode",
461 vec!["license".to_string(), "register".to_string()],
462 )?;
463 }
464 }
465 Ok(user_id)
466 }
467 }
468 fn get_ip_address(&self) -> String {
469 match self.fetch_external_ip() {
470 Ok(ip) => ip,
471 Err(_) => {
472 std::env::var("SSH_CLIENT")
473 .or_else(|_| std::env::var("REMOTE_ADDR"))
474 .unwrap_or_else(|_| "127.0.0.1".to_string())
475 .split_whitespace()
476 .next()
477 .unwrap_or("127.0.0.1")
478 .to_string()
479 }
480 }
481 }
482 fn fetch_external_ip(&self) -> Result<String> {
483 let client = blocking::Client::new();
484 let response = client
485 .get("https://api.ipify.org")
486 .timeout(std::time::Duration::from_secs(2))
487 .send()
488 .context("Failed to fetch external IP")?;
489 if response.status().is_success() {
490 let ip = response
491 .text()
492 .context("Failed to read IP response")?
493 .trim()
494 .to_string();
495 Ok(ip)
496 } else {
497 Err(anyhow::anyhow!("Failed to get external IP"))
498 }
499 }
500 pub fn get_local_license(&self) -> Result<String> {
501 let log = Log::new();
502 let home_dir = dirs::home_dir().context("Could not find home directory")?;
503 let config_dir = home_dir.join(".shipwreck");
504 let license_file = config_dir.join("license.key");
505 if !license_file.exists() {
506 log.log(
507 "No license found. Run 'cm register <license-key>' to register your Pro license",
508 vec!["license".to_string(), "get_local_license".to_string()],
509 )?;
510 return Err(
511 anyhow::anyhow!(
512 "No license found. Run 'cm register <license-key>' to register your Pro license"
513 ),
514 );
515 }
516 let license = fs::read_to_string(license_file)
517 .context("Failed to read license file")?
518 .trim()
519 .to_string();
520 Ok(license)
521 }
522 fn save_local_license(&self, license_key: &str, tier: &str) -> Result<()> {
523 let log = Log::new();
524 let home_dir = dirs::home_dir().context("Could not find home directory")?;
525 let config_dir = home_dir.join(".shipwreck");
526 fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
527 let license_file = config_dir.join("license.key");
528 fs::write(&license_file, license_key).context("Failed to save license file")?;
529 let tier_file = config_dir.join("license.tier");
530 fs::write(&tier_file, tier).context("Failed to save license tier")?;
531 let obfuscated_license = LicenseGuard::obfuscate_license(license_key);
532 if let Err(e) = LicenseGuard::store_license(&obfuscated_license) {
533 log.log(
534 "Warning: Could not store license in hidden locations",
535 vec!["license".to_string(), "save_local_license".to_string()],
536 )?;
537 }
538 log.log(
539 "License saved",
540 vec!["license".to_string(), "save_local_license".to_string()],
541 )?;
542 Ok(())
543 }
544 pub fn get_stored_license_info(&self) -> Result<(String, String)> {
545 let log = Log::new();
546 let license_key = self.get_local_license()?;
547 let home_dir = dirs::home_dir().unwrap();
548 let config_dir = home_dir.join(".shipwreck");
549 let tier_file = config_dir.join("license.tier");
550 let tier = if tier_file.exists() {
551 fs::read_to_string(tier_file)?.trim().to_string()
552 } else {
553 "FREE".to_string()
554 };
555 Ok((license_key, tier))
556 }
557 pub fn get_license_info(&self) -> Result<serde_json::Value> {
558 let license_key = self.get_local_license()?;
559 let client = blocking::Client::new();
560 let response = client
561 .post(&format!("{}/licenses/info", self.api_base_url))
562 .json(&serde_json::json!({ "license_key" : license_key }))
563 .send()
564 .context("Failed to connect to license API")?;
565 if response.status().is_success() {
566 let info: serde_json::Value = response
567 .json()
568 .context("Failed to parse license info response")?;
569 Ok(info)
570 } else {
571 Err(anyhow::anyhow!("Failed to get license info: {}", response.status()))
572 }
573 }
574 pub fn check_remaining_commands(&self) -> Result<i32> {
575 let locations = Self::get_counter_locations();
576 let mut incremented = false;
577 for (base_path, filename) in &locations {
578 if let Ok(path) = self.resolve_counter_path(base_path, filename) {
579 let current_count = if path.exists() {
580 if let Ok(content) = fs::read_to_string(&path) {
581 if let Ok(count) = self.decrypt_counter(&content) {
582 count
583 } else {
584 0
585 }
586 } else {
587 0
588 }
589 } else {
590 0
591 };
592 let new_count = current_count + 1;
593 let encrypted = self.encrypt_counter(new_count)?;
594 if fs::write(&path, &encrypted).is_ok() {
595 incremented = true;
596 }
597 }
598 }
599 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
600 let config_dir = home_dir.join(".shipwreck");
601 let old_counter_file = config_dir.join("daily_commands");
602 let old_count = if old_counter_file.exists() {
603 if let Ok(content) = fs::read_to_string(&old_counter_file) {
604 content.trim().parse::<i32>().unwrap_or(0)
605 } else {
606 0
607 }
608 } else {
609 0
610 };
611 let _ = fs::write(&old_counter_file, format!("{}", old_count + 1));
612 let validation = self.check_license_status()?;
613 if !validation.valid {
614 return Err(
615 anyhow::anyhow!(
616 validation.error.unwrap_or_else(|| "License validation failed"
617 .to_string())
618 ),
619 );
620 }
621 match validation.tier.as_str() {
622 "PRO" => Ok(-1),
623 "FREE" => Ok(validation.remaining.unwrap_or(0)),
624 _ => Ok(0),
625 }
626 }
627 pub fn is_license_expired(&self) -> Result<bool> {
628 let license_key = self.get_local_license()?;
629 let client = blocking::Client::new();
630 let response = client
631 .post(&format!("{}/licenses/check_expiration", self.api_base_url))
632 .json(&serde_json::json!({ "license_key" : license_key }))
633 .send()
634 .context("Failed to connect to license API")?;
635 if response.status().is_success() {
636 let result: serde_json::Value = response
637 .json()
638 .context("Failed to parse expiration check response")?;
639 Ok(result["expired"].as_bool().unwrap_or(true))
640 } else {
641 Ok(true)
642 }
643 }
644 fn get_counter_locations() -> Vec<(String, String)> {
645 let mut locations = Vec::new();
646 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
647 let os_type = std::env::consts::OS;
648 let is_windows = os_type == "windows";
649 let is_macos = os_type == "macos";
650 if let Ok(cargo_home) = std::env::var("CARGO_HOME") {
651 locations.push((format!("{}/bin", cargo_home), ".cmd_count".to_string()));
652 } else {
653 if is_windows {
654 locations
655 .push((
656 "%USERPROFILE%\\.cargo\\bin".to_string(),
657 ".cmd_count".to_string(),
658 ));
659 } else {
660 locations.push(("~/.cargo/bin".to_string(), ".cmd_count".to_string()));
661 }
662 }
663 if is_windows {
664 locations.push(("%APPDATA%".to_string(), ".cargo_count".to_string()));
665 } else if is_macos {
666 locations
667 .push(("~/Library/Preferences".to_string(), ".cargo_count".to_string()));
668 locations
669 .push((
670 "~/Library/Application Support/cargo-mate".to_string(),
671 ".cargo_count".to_string(),
672 ));
673 if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
674 locations
675 .push((
676 format!("{}/cargo-mate", xdg_config),
677 ".cargo_count".to_string(),
678 ));
679 }
680 } else {
681 if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
682 locations
683 .push((
684 format!("{}/cargo-mate", xdg_config),
685 ".cargo_count".to_string(),
686 ));
687 } else {
688 locations
689 .push((
690 "~/.config/cargo-mate".to_string(),
691 ".cargo_count".to_string(),
692 ));
693 locations
694 .push(("~/.cargo-mate".to_string(), ".cargo_count".to_string()));
695 }
696 }
697 if is_windows {
698 locations.push(("%LOCALAPPDATA%".to_string(), ".build_count".to_string()));
699 } else if is_macos {
700 locations
701 .push((
702 "~/Library/Application Support/cargo-mate".to_string(),
703 ".build_count".to_string(),
704 ));
705 locations
706 .push((
707 "~/Library/Preferences/cargo-mate".to_string(),
708 ".build_count".to_string(),
709 ));
710 if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
711 locations
712 .push((
713 format!("{}/cargo-mate", xdg_data),
714 ".build_count".to_string(),
715 ));
716 }
717 } else {
718 if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") {
719 locations
720 .push((
721 format!("{}/cargo-mate", xdg_data),
722 ".build_count".to_string(),
723 ));
724 } else {
725 locations
726 .push((
727 "~/.local/share/cargo-mate".to_string(),
728 ".build_count".to_string(),
729 ));
730 locations
731 .push(("~/.cargo-mate".to_string(), ".build_count".to_string()));
732 locations
733 .push((
734 "~/.local/cargo-mate".to_string(),
735 ".build_count".to_string(),
736 ));
737 }
738 }
739 if is_windows {
740 locations.push(("%TEMP%".to_string(), ".cm_usage".to_string()));
741 } else if is_macos {
742 locations
743 .push((
744 "~/Library/Caches/cargo-mate".to_string(),
745 ".cm_usage".to_string(),
746 ));
747 locations
748 .push((
749 "~/Library/Application Support/cargo-mate/cache".to_string(),
750 ".cm_usage".to_string(),
751 ));
752 if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
753 locations
754 .push((
755 format!("{}/cargo-mate", xdg_cache),
756 ".cm_usage".to_string(),
757 ));
758 }
759 } else {
760 if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
761 locations
762 .push((
763 format!("{}/cargo-mate", xdg_cache),
764 ".cm_usage".to_string(),
765 ));
766 } else {
767 locations
768 .push(("~/.cache/cargo-mate".to_string(), ".cm_usage".to_string()));
769 locations
770 .push(("~/.cargo-mate/cache".to_string(), ".cm_usage".to_string()));
771 locations.push(("/tmp/cargo-mate".to_string(), ".cm_usage".to_string()));
772 }
773 }
774 if let Ok(temp_dir) = std::env::var("TMPDIR") {
775 locations.push((temp_dir, ".daily_limit".to_string()));
776 } else if let Ok(temp_dir) = std::env::var("TEMP") {
777 locations.push((temp_dir, ".daily_limit".to_string()));
778 } else {
779 locations.push(("/tmp".to_string(), ".daily_limit".to_string()));
780 }
781 locations
782 }
783 fn get_local_command_count(&self) -> Result<i32> {
784 let mut max_count = 0;
785 let mut found_any = false;
786 let locations = Self::get_counter_locations();
787 for (base_path, filename) in &locations {
788 if let Ok(path) = self.resolve_counter_path(&base_path, &filename) {
789 if path.exists() {
790 if let Ok(content) = fs::read_to_string(&path) {
791 if let Ok(decrypted) = self.decrypt_counter(&content) {
792 max_count = max_count.max(decrypted);
793 found_any = true;
794 }
795 }
796 }
797 }
798 }
799 if found_any { Ok(max_count) } else { Ok(0) }
800 }
801 fn increment_local_command_count(&self) -> Result<()> {
802 let current_count = self.get_local_command_count()?;
803 let new_count = current_count + 1;
804 let encrypted_count = self.encrypt_counter(new_count)?;
805 let locations = Self::get_counter_locations();
806 let mut successful_stores = 0;
807 for (base_path, filename) in &locations {
808 if let Ok(path) = self.resolve_counter_path(&base_path, &filename) {
809 if let Some(parent) = path.parent() {
810 let _ = fs::create_dir_all(parent);
811 }
812 if fs::write(&path, &encrypted_count).is_ok() {
813 #[cfg(unix)]
814 {
815 if let Ok(metadata) = fs::metadata(&path) {
816 let mut perms = metadata.permissions();
817 perms.set_mode(0o400);
818 let _ = fs::set_permissions(&path, perms);
819 }
820 }
821 successful_stores += 1;
822 }
823 }
824 }
825 if successful_stores == 0 {
826 return Err(
827 anyhow::anyhow!("Failed to store command counter in any location"),
828 );
829 }
830 Ok(())
831 }
832 fn resolve_counter_path(&self, base_path: &str, filename: &str) -> Result<PathBuf> {
833 let expanded = if base_path.starts_with("~/") {
834 let home = dirs::home_dir().context("Could not find home directory")?;
835 home.join(&base_path[2..]).join(filename)
836 } else if base_path.starts_with("%") && base_path.ends_with("%") {
837 let env_var = &base_path[1..base_path.len() - 1];
838 if let Ok(env_path) = std::env::var(env_var) {
839 PathBuf::from(env_path).join(filename)
840 } else {
841 PathBuf::from(base_path).join(filename)
842 }
843 } else {
844 PathBuf::from(base_path).join(filename)
845 };
846 Ok(expanded)
847 }
848 fn encrypt_counter(&self, count: i32) -> Result<String> {
849 let salt = 0x1337;
850 let obfuscated = ((count as u32) ^ salt).to_string();
851 Ok(obfuscated.chars().rev().collect())
852 }
853 fn decrypt_counter(&self, encrypted: &str) -> Result<i32> {
854 let reversed: String = encrypted.chars().rev().collect();
855 let obfuscated: u32 = reversed.parse()?;
856 let salt = 0x1337;
857 let count = (obfuscated ^ salt) as i32;
858 Ok(count)
859 }
860 pub fn reset_local_command_count(&self) -> Result<()> {
861 let encrypted_zero = self.encrypt_counter(0)?;
862 let locations = Self::get_counter_locations();
863 for (base_path, filename) in &locations {
864 if let Ok(path) = self.resolve_counter_path(base_path, filename) {
865 if path.exists() {
866 let _ = fs::write(&path, &encrypted_zero);
867 }
868 }
869 }
870 let home_dir = dirs::home_dir().context("Could not find home directory")?;
871 let config_dir = home_dir.join(".shipwreck");
872 let old_counter_file = config_dir.join("daily_commands");
873 if old_counter_file.exists() {
874 let _ = fs::write(&old_counter_file, "0");
875 }
876 Ok(())
877 }
878 pub fn debug_command_counters(&self) -> Result<()> {
879 let log = Log::new();
880 let local_count = self.get_local_command_count()?;
881 let locations = Self::get_counter_locations();
882 log.log(
883 "🔍 Command Counter Debug:",
884 vec!["license".to_string(), "debug_command_counters".to_string()],
885 )?;
886 log.log(
887 " 📁 Local counter",
888 vec!["license".to_string(), "debug_command_counters".to_string()],
889 )?;
890 log.log(
891 " 🖥️ Platform",
892 vec!["license".to_string(), "debug_command_counters".to_string()],
893 )?;
894 log.log(
895 " 🔐 Dynamic locations",
896 vec!["license".to_string(), "debug_command_counters".to_string()],
897 )?;
898 for (base_path, filename) in &locations {
899 if let Ok(path) = self.resolve_counter_path(base_path, filename) {
900 let status = if path.exists() {
901 if let Ok(content) = fs::read_to_string(&path) {
902 if let Ok(count) = self.decrypt_counter(&content) {
903 format!("✅ {} commands", count)
904 } else {
905 "❌ Corrupted".to_string()
906 }
907 } else {
908 "❌ Unreadable".to_string()
909 }
910 } else {
911 "❌ Missing".to_string()
912 };
913 let display_path = if base_path.contains("~") {
914 format!("~{}", & base_path[1..])
915 } else {
916 base_path.to_string()
917 };
918 println!(" {} -> {}: {}", filename, display_path, status);
919 } else {
920 println!(" {} -> {}: ❌ Invalid path", filename, base_path);
921 }
922 }
923 match self.check_license_status() {
924 Ok(validation) => {
925 if validation.tier == "FREE" {
926 let api_remaining = validation.remaining.unwrap_or(0);
927 let api_used = validation.used.unwrap_or(0);
928 println!(" 🗄️ API remaining: {} commands", api_remaining);
929 println!(" 📊 API used: {} commands", api_used);
930 println!(
931 " 📈 Total API limit: {} commands", api_used + api_remaining
932 );
933 } else {
934 println!(" 🌟 PRO tier: Unlimited commands");
935 }
936 }
937 Err(e) => {
938 println!(" ❌ API counter: Unavailable ({})", e);
939 }
940 }
941 Ok(())
942 }
943 pub fn get_user_tier(&self) -> Result<String> {
944 let user_id = self.get_or_create_user_id()?;
945 let home_dir = dirs::home_dir().context("Could not find home directory")?;
946 let config_dir = home_dir.join(".shipwreck");
947 let tier_file = config_dir.join("license.tier");
948 if tier_file.exists() {
949 let tier = fs::read_to_string(&tier_file)?.trim().to_string();
950 if !tier.is_empty() {
951 return Ok(tier);
952 }
953 }
954 Ok("FREE".to_string())
955 }
956 pub fn get_remaining_commands(&self) -> Result<i32> {
957 let validation = self.check_license_status()?;
958 if validation.tier == "FREE" {
959 let api_remaining = validation.remaining.unwrap_or(0);
960 let local_count = self.get_local_command_count()?;
961 let effective_remaining = if api_remaining <= local_count {
962 api_remaining
963 } else {
964 10 - local_count
965 };
966 Ok(effective_remaining.max(0))
967 } else {
968 Ok(i32::MAX)
969 }
970 }
971}