1use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::fs;
15use std::path::PathBuf;
16use std::sync::Mutex;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19const OUTPUT_DIR: &str = "/tmp/syncable-cli/outputs";
21
22const MAX_AGE_SECS: u64 = 3600;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SessionRef {
28 pub ref_id: String,
30 pub tool: String,
32 pub contains: String,
34 pub summary: String,
36 pub timestamp: u64,
38 pub size_bytes: usize,
40}
41
42static SESSION_REGISTRY: Mutex<Vec<SessionRef>> = Mutex::new(Vec::new());
44
45pub fn register_session_ref(
47 ref_id: &str,
48 tool: &str,
49 contains: &str,
50 summary: &str,
51 size_bytes: usize,
52) {
53 if let Ok(mut registry) = SESSION_REGISTRY.lock() {
54 registry.retain(|r| r.ref_id != ref_id);
56
57 registry.push(SessionRef {
58 ref_id: ref_id.to_string(),
59 tool: tool.to_string(),
60 contains: contains.to_string(),
61 summary: summary.to_string(),
62 timestamp: SystemTime::now()
63 .duration_since(UNIX_EPOCH)
64 .map(|d| d.as_secs())
65 .unwrap_or(0),
66 size_bytes,
67 });
68 }
69}
70
71pub fn get_session_refs() -> Vec<SessionRef> {
73 SESSION_REGISTRY
74 .lock()
75 .map(|r| r.clone())
76 .unwrap_or_default()
77}
78
79pub fn cleanup_session_registry() {
81 let now = SystemTime::now()
82 .duration_since(UNIX_EPOCH)
83 .map(|d| d.as_secs())
84 .unwrap_or(0);
85
86 if let Ok(mut registry) = SESSION_REGISTRY.lock() {
87 registry.retain(|r| now - r.timestamp < MAX_AGE_SECS);
88 }
89}
90
91pub fn format_session_refs_for_agent() -> String {
93 let refs = get_session_refs();
94
95 if refs.is_empty() {
96 return String::new();
97 }
98
99 let mut output = String::from("\nš¦ AVAILABLE DATA FOR RETRIEVAL:\n");
100 output.push_str("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
101
102 for r in &refs {
103 let age = SystemTime::now()
104 .duration_since(UNIX_EPOCH)
105 .map(|d| d.as_secs())
106 .unwrap_or(0)
107 .saturating_sub(r.timestamp);
108
109 let age_str = if age < 60 {
110 format!("{}s ago", age)
111 } else {
112 format!("{}m ago", age / 60)
113 };
114
115 output.push_str(&format!(
116 "\n⢠{} [{}]\n Contains: {}\n Summary: {}\n Retrieve: retrieve_output(\"{}\") or with query\n",
117 r.ref_id, age_str, r.contains, r.summary, r.ref_id
118 ));
119 }
120
121 output.push_str("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
122 output.push_str(
123 "Query examples: \"severity:critical\", \"file:deployment.yaml\", \"code:DL3008\"\n",
124 );
125
126 output
127}
128
129fn generate_ref_id() -> String {
131 let timestamp = SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .map(|d| d.as_millis())
134 .unwrap_or(0);
135
136 let ts_part = format!("{:x}", timestamp)
138 .chars()
139 .rev()
140 .take(6)
141 .collect::<String>();
142 let rand_part: String = (0..4)
143 .map(|_| {
144 let idx = (timestamp as usize + rand_simple()) % 36;
145 "abcdefghijklmnopqrstuvwxyz0123456789"
146 .chars()
147 .nth(idx)
148 .unwrap()
149 })
150 .collect();
151
152 format!("{}_{}", ts_part, rand_part)
153}
154
155fn rand_simple() -> usize {
157 let ptr = Box::into_raw(Box::new(0u8));
158 let addr = ptr as usize;
159 unsafe { drop(Box::from_raw(ptr)) };
160 addr.wrapping_mul(1103515245).wrapping_add(12345) % (1 << 31)
161}
162
163fn ensure_output_dir() -> std::io::Result<PathBuf> {
165 let path = PathBuf::from(OUTPUT_DIR);
166 if !path.exists() {
167 fs::create_dir_all(&path)?;
168 }
169 Ok(path)
170}
171
172pub fn store_output(output: &Value, tool_name: &str) -> String {
181 let ref_id = format!("{}_{}", tool_name, generate_ref_id());
182
183 if let Ok(dir) = ensure_output_dir() {
184 let path = dir.join(format!("{}.json", ref_id));
185
186 let stored = serde_json::json!({
188 "ref_id": ref_id,
189 "tool": tool_name,
190 "timestamp": SystemTime::now()
191 .duration_since(UNIX_EPOCH)
192 .map(|d| d.as_secs())
193 .unwrap_or(0),
194 "data": output
195 });
196
197 if let Ok(json_str) = serde_json::to_string(&stored) {
198 let _ = fs::write(&path, json_str);
199 }
200 }
201
202 ref_id
203}
204
205pub fn retrieve_output(ref_id: &str) -> Option<Value> {
213 let path = PathBuf::from(OUTPUT_DIR).join(format!("{}.json", ref_id));
214
215 if !path.exists() {
216 return None;
217 }
218
219 let content = fs::read_to_string(&path).ok()?;
220 let stored: Value = serde_json::from_str(&content).ok()?;
221
222 stored.get("data").cloned()
224}
225
226pub fn retrieve_filtered(ref_id: &str, query: Option<&str>) -> Option<Value> {
247 let data = retrieve_output(ref_id)?;
248
249 if is_analyze_project_output(&data) {
251 return retrieve_analyze_project(&data, query);
252 }
253
254 let query = match query {
255 Some(q) if !q.is_empty() => q,
256 _ => return Some(data),
257 };
258
259 let (filter_type, filter_value) = parse_query(query);
261
262 let issues = find_issues_array(&data)?;
264
265 let filtered: Vec<Value> = issues
267 .iter()
268 .filter(|issue| matches_filter(issue, &filter_type, &filter_value))
269 .cloned()
270 .collect();
271
272 Some(serde_json::json!({
273 "query": query,
274 "total_matches": filtered.len(),
275 "results": filtered
276 }))
277}
278
279fn parse_query(query: &str) -> (String, String) {
281 if let Some(idx) = query.find(':') {
282 let (t, v) = query.split_at(idx);
283 (t.to_lowercase(), v[1..].to_string())
284 } else {
285 ("any".to_string(), query.to_string())
287 }
288}
289
290fn find_issues_array(data: &Value) -> Option<Vec<Value>> {
292 let issue_fields = [
293 "issues",
294 "findings",
295 "violations",
296 "warnings",
297 "errors",
298 "recommendations",
299 "results",
300 ];
301
302 for field in &issue_fields {
303 if let Some(arr) = data.get(field).and_then(|v| v.as_array()) {
304 return Some(arr.clone());
305 }
306 }
307
308 if let Some(arr) = data.as_array() {
310 return Some(arr.clone());
311 }
312
313 None
314}
315
316fn matches_filter(issue: &Value, filter_type: &str, filter_value: &str) -> bool {
318 match filter_type {
319 "severity" | "level" => {
320 let sev = issue
321 .get("severity")
322 .or_else(|| issue.get("level"))
323 .and_then(|v| v.as_str())
324 .unwrap_or("");
325 sev.to_lowercase().contains(&filter_value.to_lowercase())
326 }
327 "file" | "path" => {
328 let file = issue
329 .get("file")
330 .or_else(|| issue.get("path"))
331 .or_else(|| issue.get("filename"))
332 .and_then(|v| v.as_str())
333 .unwrap_or("");
334 file.to_lowercase().contains(&filter_value.to_lowercase())
335 }
336 "code" | "rule" => {
337 let code = issue
338 .get("code")
339 .or_else(|| issue.get("rule"))
340 .or_else(|| issue.get("rule_id"))
341 .and_then(|v| v.as_str())
342 .unwrap_or("");
343 code.to_lowercase().contains(&filter_value.to_lowercase())
344 }
345 "container" | "resource" => {
346 let container = issue
347 .get("container")
348 .or_else(|| issue.get("resource"))
349 .or_else(|| issue.get("name"))
350 .and_then(|v| v.as_str())
351 .unwrap_or("");
352 container
353 .to_lowercase()
354 .contains(&filter_value.to_lowercase())
355 }
356 "any" | _ => {
357 let issue_str = serde_json::to_string(issue).unwrap_or_default();
359 issue_str
360 .to_lowercase()
361 .contains(&filter_value.to_lowercase())
362 }
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum OutputType {
373 MonorepoAnalysis,
375 ProjectAnalysis,
377 LintResult,
379 OptimizationResult,
381 Generic,
383}
384
385pub fn detect_output_type(data: &Value) -> OutputType {
387 if data.get("projects").is_some() || data.get("is_monorepo").is_some() {
389 return OutputType::MonorepoAnalysis;
390 }
391
392 if data.get("languages").is_some() && data.get("analysis_metadata").is_some() {
394 return OutputType::ProjectAnalysis;
395 }
396
397 if data.get("failures").is_some() {
399 return OutputType::LintResult;
400 }
401
402 if data.get("recommendations").is_some() {
404 return OutputType::OptimizationResult;
405 }
406
407 OutputType::Generic
408}
409
410fn is_analyze_project_output(data: &Value) -> bool {
412 matches!(
413 detect_output_type(data),
414 OutputType::MonorepoAnalysis | OutputType::ProjectAnalysis
415 )
416}
417
418pub fn retrieve_analyze_project(data: &Value, query: Option<&str>) -> Option<Value> {
428 let query = query.unwrap_or("compact:true");
429 let (query_type, query_value) = parse_query(query);
430
431 match query_type.as_str() {
432 "section" => match query_value.as_str() {
433 "summary" => Some(extract_summary(data)),
434 "projects" => Some(extract_projects_list(data)),
435 "frameworks" => Some(extract_all_frameworks(data)),
436 "languages" => Some(extract_all_languages(data)),
437 "services" => Some(extract_all_services(data)),
438 _ => Some(compact_analyze_output(data)),
439 },
440 "project" => extract_project_by_name(data, &query_value),
441 "service" => extract_service_by_name(data, &query_value),
442 "language" => extract_language_details(data, &query_value),
443 "framework" => extract_framework_details(data, &query_value),
444 "compact" => Some(compact_analyze_output(data)),
445 _ => {
446 Some(compact_analyze_output(data))
448 }
449 }
450}
451
452fn extract_summary(data: &Value) -> Value {
454 let mut summary = serde_json::Map::new();
455
456 if let Some(root) = data.get("root_path").and_then(|v| v.as_str()) {
458 summary.insert("root_path".to_string(), Value::String(root.to_string()));
459 }
460 if let Some(mono) = data.get("is_monorepo").and_then(|v| v.as_bool()) {
461 summary.insert("is_monorepo".to_string(), Value::Bool(mono));
462 }
463
464 if let Some(root) = data.get("project_root").and_then(|v| v.as_str()) {
466 summary.insert("project_root".to_string(), Value::String(root.to_string()));
467 }
468 if let Some(arch) = data.get("architecture_type").and_then(|v| v.as_str()) {
469 summary.insert("architecture_type".to_string(), Value::String(arch.to_string()));
470 }
471
472 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
474 summary.insert("project_count".to_string(), Value::Number(projects.len().into()));
475
476 let names: Vec<Value> = projects
478 .iter()
479 .filter_map(|p| p.get("name").and_then(|n| n.as_str()))
480 .map(|n| Value::String(n.to_string()))
481 .collect();
482 summary.insert("project_names".to_string(), Value::Array(names));
483 }
484
485 if let Some(languages) = data.get("languages").and_then(|v| v.as_array()) {
487 let names: Vec<Value> = languages
488 .iter()
489 .filter_map(|l| l.get("name").and_then(|n| n.as_str()))
490 .map(|n| Value::String(n.to_string()))
491 .collect();
492 summary.insert("languages".to_string(), Value::Array(names));
493 }
494
495 if let Some(techs) = data.get("technologies").and_then(|v| v.as_array()) {
497 let names: Vec<Value> = techs
498 .iter()
499 .filter_map(|t| t.get("name").and_then(|n| n.as_str()))
500 .map(|n| Value::String(n.to_string()))
501 .collect();
502 summary.insert("technologies".to_string(), Value::Array(names));
503 }
504
505 if let Some(services) = data.get("services").and_then(|v| v.as_array()) {
507 summary.insert("services_count".to_string(), Value::Number(services.len().into()));
508 let service_names: Vec<Value> = services
510 .iter()
511 .filter_map(|s| s.get("name").and_then(|n| n.as_str()))
512 .map(|n| Value::String(n.to_string()))
513 .collect();
514 if !service_names.is_empty() {
515 summary.insert("services".to_string(), Value::Array(service_names));
516 }
517 }
518
519 Value::Object(summary)
520}
521
522fn extract_projects_list(data: &Value) -> Value {
524 let projects = data.get("projects").and_then(|v| v.as_array());
525
526 let list: Vec<Value> = projects
527 .map(|arr| {
528 arr.iter()
529 .map(|p| {
530 let mut proj = serde_json::Map::new();
531 if let Some(name) = p.get("name") {
532 proj.insert("name".to_string(), name.clone());
533 }
534 if let Some(path) = p.get("path") {
535 proj.insert("path".to_string(), path.clone());
536 }
537 if let Some(cat) = p.get("project_category") {
538 proj.insert("category".to_string(), cat.clone());
539 }
540 if let Some(analysis) = p.get("analysis") {
542 if let Some(langs) = analysis.get("languages").and_then(|v| v.as_array()) {
543 let lang_names: Vec<Value> = langs
544 .iter()
545 .filter_map(|l| l.get("name").and_then(|n| n.as_str()))
546 .map(|n| Value::String(n.to_string()))
547 .collect();
548 proj.insert("languages".to_string(), Value::Array(lang_names));
549 }
550 if let Some(fws) = analysis.get("frameworks").and_then(|v| v.as_array()) {
551 let fw_names: Vec<Value> = fws
552 .iter()
553 .filter_map(|f| f.get("name").and_then(|n| n.as_str()))
554 .map(|n| Value::String(n.to_string()))
555 .collect();
556 proj.insert("frameworks".to_string(), Value::Array(fw_names));
557 }
558 }
559 Value::Object(proj)
560 })
561 .collect()
562 })
563 .unwrap_or_default();
564
565 serde_json::json!({
566 "total_projects": list.len(),
567 "projects": list
568 })
569}
570
571fn extract_project_by_name(data: &Value, name: &str) -> Option<Value> {
573 let projects = data.get("projects").and_then(|v| v.as_array())?;
574
575 let project = projects.iter().find(|p| {
576 p.get("name")
577 .and_then(|n| n.as_str())
578 .map(|n| n.to_lowercase().contains(&name.to_lowercase()))
579 .unwrap_or(false)
580 })?;
581
582 Some(compact_project(project))
583}
584
585fn extract_service_by_name(data: &Value, name: &str) -> Option<Value> {
587 let projects = data.get("projects").and_then(|v| v.as_array())?;
588
589 for project in projects {
590 if let Some(services) = project
591 .get("analysis")
592 .and_then(|a| a.get("services"))
593 .and_then(|s| s.as_array())
594 {
595 if let Some(service) = services.iter().find(|s| {
596 s.get("name")
597 .and_then(|n| n.as_str())
598 .map(|n| n.to_lowercase().contains(&name.to_lowercase()))
599 .unwrap_or(false)
600 }) {
601 return Some(service.clone());
602 }
603 }
604 }
605 None
606}
607
608fn extract_language_details(data: &Value, lang_name: &str) -> Option<Value> {
610 let mut results = Vec::new();
611
612 let process_languages = |languages: &[Value], proj_name: &str, results: &mut Vec<Value>| {
614 for lang in languages {
615 let name = lang.get("name").and_then(|n| n.as_str()).unwrap_or("");
616 if lang_name == "*" || name.to_lowercase().contains(&lang_name.to_lowercase()) {
617 let mut compact_lang = serde_json::Map::new();
618 if !proj_name.is_empty() {
619 compact_lang.insert("project".to_string(), Value::String(proj_name.to_string()));
620 }
621 compact_lang.insert("name".to_string(), lang.get("name").cloned().unwrap_or(Value::Null));
622 compact_lang.insert("version".to_string(), lang.get("version").cloned().unwrap_or(Value::Null));
623 compact_lang.insert("confidence".to_string(), lang.get("confidence").cloned().unwrap_or(Value::Null));
624
625 if let Some(files) = lang.get("files").and_then(|f| f.as_array()) {
627 compact_lang.insert("file_count".to_string(), Value::Number(files.len().into()));
628 }
629
630 results.push(Value::Object(compact_lang));
631 }
632 }
633 };
634
635 if let Some(languages) = data.get("languages").and_then(|v| v.as_array()) {
637 process_languages(languages, "", &mut results);
638 }
639
640 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
642 for project in projects {
643 let proj_name = project
644 .get("name")
645 .and_then(|n| n.as_str())
646 .unwrap_or("unknown");
647
648 if let Some(languages) = project
649 .get("analysis")
650 .and_then(|a| a.get("languages"))
651 .and_then(|l| l.as_array())
652 {
653 process_languages(languages, proj_name, &mut results);
654 }
655 }
656 }
657
658 Some(serde_json::json!({
659 "query": format!("language:{}", lang_name),
660 "total_matches": results.len(),
661 "results": results
662 }))
663}
664
665fn extract_framework_details(data: &Value, fw_name: &str) -> Option<Value> {
667 let mut results = Vec::new();
668
669 let process_techs = |techs: &[Value], proj_name: &str, results: &mut Vec<Value>| {
671 for tech in techs {
672 let name = tech.get("name").and_then(|n| n.as_str()).unwrap_or("");
673 if fw_name == "*" || name.to_lowercase().contains(&fw_name.to_lowercase()) {
674 let mut compact_fw = serde_json::Map::new();
675 if !proj_name.is_empty() {
676 compact_fw.insert("project".to_string(), Value::String(proj_name.to_string()));
677 }
678 if let Some(v) = tech.get("name") {
679 compact_fw.insert("name".to_string(), v.clone());
680 }
681 if let Some(v) = tech.get("version") {
682 compact_fw.insert("version".to_string(), v.clone());
683 }
684 if let Some(v) = tech.get("category") {
685 compact_fw.insert("category".to_string(), v.clone());
686 }
687 results.push(Value::Object(compact_fw));
688 }
689 }
690 };
691
692 if let Some(techs) = data.get("technologies").and_then(|v| v.as_array()) {
694 process_techs(techs, "", &mut results);
695 }
696
697 if let Some(fws) = data.get("frameworks").and_then(|v| v.as_array()) {
699 process_techs(fws, "", &mut results);
700 }
701
702 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
704 for project in projects {
705 let proj_name = project
706 .get("name")
707 .and_then(|n| n.as_str())
708 .unwrap_or("unknown");
709
710 if let Some(frameworks) = project
711 .get("analysis")
712 .and_then(|a| a.get("frameworks"))
713 .and_then(|f| f.as_array())
714 {
715 process_techs(frameworks, proj_name, &mut results);
716 }
717 }
718 }
719
720 Some(serde_json::json!({
721 "query": format!("framework:{}", fw_name),
722 "total_matches": results.len(),
723 "results": results
724 }))
725}
726
727fn extract_all_frameworks(data: &Value) -> Value {
729 extract_framework_details(data, "*").unwrap_or(serde_json::json!({"results": []}))
730}
731
732fn extract_all_languages(data: &Value) -> Value {
734 extract_language_details(data, "*").unwrap_or(serde_json::json!({"results": []}))
735}
736
737fn extract_all_services(data: &Value) -> Value {
740 extract_projects_list(data)
743}
744
745fn compact_analyze_output(data: &Value) -> Value {
747 let mut result = serde_json::Map::new();
748
749 if let Some(v) = data.get("root_path") {
751 result.insert("root_path".to_string(), v.clone());
752 }
753 if let Some(v) = data.get("is_monorepo") {
754 result.insert("is_monorepo".to_string(), v.clone());
755 }
756
757 if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
759 let compacted: Vec<Value> = projects.iter().map(|p| compact_project(p)).collect();
760 result.insert("projects".to_string(), Value::Array(compacted));
761 return Value::Object(result);
762 }
763
764 if let Some(v) = data.get("project_root") {
766 result.insert("project_root".to_string(), v.clone());
767 }
768 if let Some(v) = data.get("architecture_type") {
769 result.insert("architecture_type".to_string(), v.clone());
770 }
771 if let Some(v) = data.get("project_type") {
772 result.insert("project_type".to_string(), v.clone());
773 }
774
775 if let Some(languages) = data.get("languages").and_then(|v| v.as_array()) {
777 let compacted: Vec<Value> = languages
778 .iter()
779 .map(|lang| {
780 let mut compact_lang = serde_json::Map::new();
781 for key in &["name", "version", "confidence"] {
782 if let Some(v) = lang.get(*key) {
783 compact_lang.insert(key.to_string(), v.clone());
784 }
785 }
786 if let Some(files) = lang.get("files").and_then(|f| f.as_array()) {
788 compact_lang.insert("file_count".to_string(), Value::Number(files.len().into()));
789 }
790 Value::Object(compact_lang)
791 })
792 .collect();
793 result.insert("languages".to_string(), Value::Array(compacted));
794 }
795
796 if let Some(techs) = data.get("technologies").and_then(|v| v.as_array()) {
798 let compacted: Vec<Value> = techs
799 .iter()
800 .map(|tech| {
801 let mut compact_tech = serde_json::Map::new();
802 for key in &["name", "version", "category", "confidence"] {
803 if let Some(v) = tech.get(*key) {
804 compact_tech.insert(key.to_string(), v.clone());
805 }
806 }
807 Value::Object(compact_tech)
808 })
809 .collect();
810 result.insert("technologies".to_string(), Value::Array(compacted));
811 }
812
813 if let Some(services) = data.get("services").and_then(|v| v.as_array()) {
815 result.insert("services".to_string(), Value::Array(services.clone()));
816 }
817
818 if let Some(meta) = data.get("analysis_metadata") {
820 result.insert("analysis_metadata".to_string(), meta.clone());
821 }
822
823 Value::Object(result)
824}
825
826fn compact_project(project: &Value) -> Value {
828 let mut compact = serde_json::Map::new();
829
830 for key in &["name", "path", "project_category"] {
832 if let Some(v) = project.get(*key) {
833 compact.insert(key.to_string(), v.clone());
834 }
835 }
836
837 if let Some(analysis) = project.get("analysis") {
839 let mut compact_analysis = serde_json::Map::new();
840
841 if let Some(v) = analysis.get("project_root") {
843 compact_analysis.insert("project_root".to_string(), v.clone());
844 }
845
846 if let Some(languages) = analysis.get("languages").and_then(|v| v.as_array()) {
848 let compacted: Vec<Value> = languages
849 .iter()
850 .map(|lang| {
851 let mut compact_lang = serde_json::Map::new();
852 for key in &["name", "version", "confidence"] {
853 if let Some(v) = lang.get(*key) {
854 compact_lang.insert(key.to_string(), v.clone());
855 }
856 }
857 if let Some(files) = lang.get("files").and_then(|f| f.as_array()) {
859 compact_lang.insert("file_count".to_string(), Value::Number(files.len().into()));
860 }
861 Value::Object(compact_lang)
862 })
863 .collect();
864 compact_analysis.insert("languages".to_string(), Value::Array(compacted));
865 }
866
867 for key in &["frameworks", "databases", "services", "build_tools", "package_managers"] {
869 if let Some(v) = analysis.get(*key) {
870 compact_analysis.insert(key.to_string(), v.clone());
871 }
872 }
873
874 compact.insert("analysis".to_string(), Value::Object(compact_analysis));
875 }
876
877 Value::Object(compact)
878}
879
880pub fn list_outputs() -> Vec<OutputInfo> {
882 let dir = match ensure_output_dir() {
883 Ok(d) => d,
884 Err(_) => return Vec::new(),
885 };
886
887 let mut outputs = Vec::new();
888
889 if let Ok(entries) = fs::read_dir(&dir) {
890 for entry in entries.flatten() {
891 if let Some(filename) = entry.file_name().to_str() {
892 if filename.ends_with(".json") {
893 let ref_id = filename.trim_end_matches(".json").to_string();
894
895 if let Ok(content) = fs::read_to_string(entry.path()) {
897 if let Ok(stored) = serde_json::from_str::<Value>(&content) {
898 let tool = stored
899 .get("tool")
900 .and_then(|v| v.as_str())
901 .unwrap_or("unknown")
902 .to_string();
903 let timestamp = stored
904 .get("timestamp")
905 .and_then(|v| v.as_u64())
906 .unwrap_or(0);
907 let size = content.len();
908
909 outputs.push(OutputInfo {
910 ref_id,
911 tool,
912 timestamp,
913 size_bytes: size,
914 });
915 }
916 }
917 }
918 }
919 }
920 }
921
922 outputs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
924 outputs
925}
926
927#[derive(Debug, Clone)]
929pub struct OutputInfo {
930 pub ref_id: String,
931 pub tool: String,
932 pub timestamp: u64,
933 pub size_bytes: usize,
934}
935
936pub fn cleanup_old_outputs() {
938 let dir = match ensure_output_dir() {
939 Ok(d) => d,
940 Err(_) => return,
941 };
942
943 let now = SystemTime::now()
944 .duration_since(UNIX_EPOCH)
945 .map(|d| d.as_secs())
946 .unwrap_or(0);
947
948 if let Ok(entries) = fs::read_dir(&dir) {
949 for entry in entries.flatten() {
950 if let Ok(content) = fs::read_to_string(entry.path()) {
951 if let Ok(stored) = serde_json::from_str::<Value>(&content) {
952 let timestamp = stored
953 .get("timestamp")
954 .and_then(|v| v.as_u64())
955 .unwrap_or(0);
956
957 if now - timestamp > MAX_AGE_SECS {
958 let _ = fs::remove_file(entry.path());
959 }
960 }
961 }
962 }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn test_store_and_retrieve() {
972 let data = serde_json::json!({
973 "issues": [
974 { "code": "test1", "severity": "high", "file": "test.yaml" }
975 ]
976 });
977
978 let ref_id = store_output(&data, "test_tool");
979 assert!(ref_id.starts_with("test_tool_"));
980
981 let retrieved = retrieve_output(&ref_id);
982 assert!(retrieved.is_some());
983 assert_eq!(retrieved.unwrap(), data);
984 }
985
986 #[test]
987 fn test_filtered_retrieval() {
988 let data = serde_json::json!({
989 "issues": [
990 { "code": "DL3008", "severity": "warning", "file": "Dockerfile1" },
991 { "code": "DL3009", "severity": "info", "file": "Dockerfile2" },
992 { "code": "DL3008", "severity": "warning", "file": "Dockerfile3" }
993 ]
994 });
995
996 let ref_id = store_output(&data, "filter_test");
997
998 let filtered = retrieve_filtered(&ref_id, Some("code:DL3008"));
1000 assert!(filtered.is_some());
1001 let results = filtered.unwrap();
1002 assert_eq!(results["total_matches"], 2);
1003
1004 let filtered = retrieve_filtered(&ref_id, Some("severity:info"));
1006 assert!(filtered.is_some());
1007 let results = filtered.unwrap();
1008 assert_eq!(results["total_matches"], 1);
1009 }
1010
1011 #[test]
1012 fn test_parse_query() {
1013 assert_eq!(
1014 parse_query("severity:critical"),
1015 ("severity".to_string(), "critical".to_string())
1016 );
1017 assert_eq!(
1018 parse_query("searchterm"),
1019 ("any".to_string(), "searchterm".to_string())
1020 );
1021 }
1022
1023 #[test]
1024 fn test_analyze_project_detection() {
1025 let analyze_data = serde_json::json!({
1026 "root_path": "/test",
1027 "is_monorepo": true,
1028 "projects": []
1029 });
1030 assert!(is_analyze_project_output(&analyze_data));
1031
1032 let lint_data = serde_json::json!({
1033 "issues": [{ "code": "DL3008" }]
1034 });
1035 assert!(!is_analyze_project_output(&lint_data));
1036 }
1037
1038 #[test]
1039 fn test_analyze_project_summary() {
1040 let data = serde_json::json!({
1041 "root_path": "/test/monorepo",
1042 "is_monorepo": true,
1043 "projects": [
1044 { "name": "api-gateway", "path": "services/api" },
1045 { "name": "web-app", "path": "apps/web" }
1046 ]
1047 });
1048
1049 let summary = extract_summary(&data);
1050 assert_eq!(summary["root_path"], "/test/monorepo");
1051 assert_eq!(summary["is_monorepo"], true);
1052 assert_eq!(summary["project_count"], 2);
1053 }
1054
1055 #[test]
1056 fn test_analyze_project_compact() {
1057 let files: Vec<String> = (0..1000).map(|i| format!("/src/file{}.ts", i)).collect();
1059
1060 let data = serde_json::json!({
1061 "root_path": "/test",
1062 "is_monorepo": false,
1063 "projects": [{
1064 "name": "test-project",
1065 "path": "",
1066 "project_category": "Api",
1067 "analysis": {
1068 "project_root": "/test",
1069 "languages": [{
1070 "name": "TypeScript",
1071 "version": "5.0",
1072 "confidence": 0.95,
1073 "files": files
1074 }],
1075 "frameworks": [{
1076 "name": "React",
1077 "version": "18.0"
1078 }]
1079 }
1080 }]
1081 });
1082
1083 let ref_id = store_output(&data, "analyze_project_test");
1084
1085 let result = retrieve_filtered(&ref_id, None);
1087 assert!(result.is_some());
1088
1089 let compacted = result.unwrap();
1090
1091 let project = &compacted["projects"][0];
1093 let lang = &project["analysis"]["languages"][0];
1094 assert_eq!(lang["name"], "TypeScript");
1095 assert_eq!(lang["file_count"], 1000);
1096 assert!(lang.get("files").is_none()); let compacted_str = serde_json::to_string(&compacted).unwrap();
1100 let original_str = serde_json::to_string(&data).unwrap();
1101 assert!(compacted_str.len() < original_str.len() / 10); }
1103
1104 #[test]
1105 fn test_analyze_project_section_queries() {
1106 let data = serde_json::json!({
1107 "root_path": "/test",
1108 "is_monorepo": true,
1109 "projects": [{
1110 "name": "api-service",
1111 "path": "services/api",
1112 "project_category": "Api",
1113 "analysis": {
1114 "languages": [{
1115 "name": "Go",
1116 "version": "1.21",
1117 "confidence": 0.9,
1118 "files": ["/main.go", "/handler.go"]
1119 }],
1120 "frameworks": [{
1121 "name": "Gin",
1122 "version": "1.9",
1123 "category": "Web"
1124 }],
1125 "services": [{
1126 "name": "api-http",
1127 "type": "http",
1128 "port": 8080
1129 }]
1130 }
1131 }]
1132 });
1133
1134 let ref_id = store_output(&data, "analyze_query_test");
1135
1136 let projects = retrieve_filtered(&ref_id, Some("section:projects"));
1138 assert!(projects.is_some());
1139 assert_eq!(projects.as_ref().unwrap()["total_projects"], 1);
1140
1141 let frameworks = retrieve_filtered(&ref_id, Some("section:frameworks"));
1143 assert!(frameworks.is_some());
1144 assert_eq!(frameworks.as_ref().unwrap()["total_matches"], 1);
1145 assert_eq!(frameworks.as_ref().unwrap()["results"][0]["name"], "Gin");
1146
1147 let languages = retrieve_filtered(&ref_id, Some("section:languages"));
1149 assert!(languages.is_some());
1150 assert_eq!(languages.as_ref().unwrap()["total_matches"], 1);
1151 assert_eq!(languages.as_ref().unwrap()["results"][0]["name"], "Go");
1152 assert_eq!(languages.as_ref().unwrap()["results"][0]["file_count"], 2);
1154
1155 let go = retrieve_filtered(&ref_id, Some("language:Go"));
1157 assert!(go.is_some());
1158 assert_eq!(go.as_ref().unwrap()["total_matches"], 1);
1159
1160 let gin = retrieve_filtered(&ref_id, Some("framework:Gin"));
1162 assert!(gin.is_some());
1163 assert_eq!(gin.as_ref().unwrap()["total_matches"], 1);
1164 }
1165}