1use super::{hex::HexFormatter, Formatter, PathDisplayMode, StreamingFormatter};
2use crate::context::detect_project_context;
3use crate::scanner::{FileNode, TreeStats};
4use anyhow::Result;
5use sha2::{Digest, Sha256};
6use std::io::Write;
7use std::path::Path;
8
9pub struct AiFormatter {
10 hex_formatter: HexFormatter,
11}
12
13impl AiFormatter {
14 pub fn new(no_emoji: bool, _path_mode: PathDisplayMode) -> Self {
15 Self {
16 hex_formatter: HexFormatter::new(false, no_emoji, true, PathDisplayMode::Off, false),
19 }
20 }
21
22 fn calculate_tree_hash(&self, nodes: &[FileNode]) -> String {
24 let mut hasher = Sha256::new();
25
26 for node in nodes {
28 hasher.update(node.depth.to_le_bytes());
30 hasher.update(
31 node.path
32 .file_name()
33 .unwrap_or_default()
34 .to_string_lossy()
35 .as_bytes(),
36 );
37 hasher.update([if node.is_dir { 1 } else { 0 }]);
38 hasher.update(node.size.to_le_bytes());
39 hasher.update(node.permissions.to_le_bytes());
40 }
41
42 let result = hasher.finalize();
44 hex::encode(&result[..8])
45 }
46}
47
48impl Formatter for AiFormatter {
49 fn format(
50 &self,
51 writer: &mut dyn Write,
52 nodes: &[FileNode],
53 stats: &TreeStats,
54 root_path: &Path,
55 ) -> Result<()> {
56 writeln!(writer, "TREE_HEX_V1:")?;
58
59 if let Some(context) = detect_project_context(root_path) {
61 writeln!(writer, "CONTEXT: {}", context)?;
62 }
63
64 let tree_hash = self.calculate_tree_hash(nodes);
66 writeln!(writer, "HASH: {}", tree_hash)?;
67
68 self.hex_formatter.format(writer, nodes, stats, root_path)?;
70
71 writeln!(writer, "\nSTATS:")?;
73 writeln!(
74 writer,
75 "F:{:x} D:{:x} S:{:x} ({:.1}MB)",
76 stats.total_files,
77 stats.total_dirs,
78 stats.total_size,
79 stats.total_size as f64 / (1024.0 * 1024.0)
80 )?;
81
82 if !stats.file_types.is_empty() {
84 let mut types: Vec<_> = stats.file_types.iter().collect();
85 types.sort_by(|a, b| b.1.cmp(a.1));
86
87 let types_str: Vec<String> = types
88 .iter()
89 .take(10)
90 .map(|(ext, count)| format!("{}:{:x}", ext, count))
91 .collect();
92
93 writeln!(writer, "TYPES: {}", types_str.join(" "))?;
94 }
95
96 if !stats.largest_files.is_empty() {
98 let large_str: Vec<String> = stats
99 .largest_files
100 .iter()
101 .take(5)
102 .map(|(size, path)| {
103 let name = path
104 .file_name()
105 .unwrap_or(path.as_os_str())
106 .to_string_lossy();
107 format!("{}:{:x}", name, size)
108 })
109 .collect();
110
111 writeln!(writer, "LARGE: {}", large_str.join(" "))?;
112 }
113
114 if !stats.oldest_files.is_empty() && !stats.newest_files.is_empty() {
116 let oldest = stats.oldest_files[0]
117 .0
118 .duration_since(std::time::UNIX_EPOCH)
119 .unwrap_or_default()
120 .as_secs();
121 let newest = stats.newest_files[0]
122 .0
123 .duration_since(std::time::UNIX_EPOCH)
124 .unwrap_or_default()
125 .as_secs();
126
127 writeln!(writer, "DATES: {:x}-{:x}", oldest, newest)?;
128 }
129
130 writeln!(writer, "END_AI")?;
131
132 Ok(())
133 }
134}
135
136impl StreamingFormatter for AiFormatter {
137 fn start_stream(&self, writer: &mut dyn Write, root_path: &Path) -> Result<()> {
138 writeln!(writer, "TREE_HEX_V1:")?;
140
141 if let Some(context) = detect_project_context(root_path) {
143 writeln!(writer, "CONTEXT: {}", context)?;
144 }
145
146 writeln!(writer, "HASH: STREAMING")?;
148 writer.flush()?;
149 Ok(())
150 }
151
152 fn format_node(&self, writer: &mut dyn Write, node: &FileNode, root_path: &Path) -> Result<()> {
153 self.hex_formatter.format_node(writer, node, root_path)
154 }
155
156 fn end_stream(
157 &self,
158 writer: &mut dyn Write,
159 stats: &TreeStats,
160 _root_path: &Path,
161 ) -> Result<()> {
162 writeln!(writer, "\nSTATS:")?;
164 writeln!(
165 writer,
166 "F:{:x} D:{:x} S:{:x} ({:.1}MB)",
167 stats.total_files,
168 stats.total_dirs,
169 stats.total_size,
170 stats.total_size as f64 / (1024.0 * 1024.0)
171 )?;
172
173 if !stats.file_types.is_empty() {
175 let mut types: Vec<_> = stats.file_types.iter().collect();
176 types.sort_by(|a, b| b.1.cmp(a.1));
177
178 let types_str: Vec<String> = types
179 .iter()
180 .take(10)
181 .map(|(ext, count)| format!("{}:{:x}", ext, count))
182 .collect();
183
184 writeln!(writer, "TYPES: {}", types_str.join(" "))?;
185 }
186
187 if !stats.largest_files.is_empty() {
189 let large_str: Vec<String> = stats
190 .largest_files
191 .iter()
192 .take(5)
193 .map(|(size, path)| {
194 let name = path
195 .file_name()
196 .unwrap_or(path.as_os_str())
197 .to_string_lossy();
198 format!("{}:{:x}", name, size)
199 })
200 .collect();
201
202 writeln!(writer, "LARGE: {}", large_str.join(" "))?;
203 }
204
205 if !stats.oldest_files.is_empty() && !stats.newest_files.is_empty() {
207 let oldest = stats.oldest_files[0]
208 .0
209 .duration_since(std::time::UNIX_EPOCH)
210 .unwrap_or_default()
211 .as_secs();
212 let newest = stats.newest_files[0]
213 .0
214 .duration_since(std::time::UNIX_EPOCH)
215 .unwrap_or_default()
216 .as_secs();
217
218 writeln!(writer, "DATES: {:x}-{:x}", oldest, newest)?;
219 }
220
221 writeln!(writer, "END_AI")?;
222 writer.flush()?;
223
224 Ok(())
225 }
226}