1use crate::config::Config;
3use crate::config::load_bundle;
4use crate::profile::{FamilyName, resolve_profile_for_family};
5use crate::{
6 FragmentOutput, Framing, PrompterError, RenderOutput, default_post_prompt, default_pre_prompt,
7 format_system_prefix,
8};
9use chrono::Local;
10use std::collections::HashSet;
11use std::env;
12use std::fs;
13use std::io::{self, Write};
14use std::path::{Path, PathBuf};
15use tftio_cli_common::{JsonOutput, render_response};
16
17pub fn render_to_writer(
19 cfg: &Config,
20 mut w: impl Write,
21 profiles: &[String],
22 family: Option<&FamilyName>,
23 separator: Option<&str>,
24 pre_prompt: Option<&str>,
25 post_prompt: Option<&str>,
26 framing: Framing,
27 output: JsonOutput,
28) -> Result<(), PrompterError> {
29 let mut seen_files = HashSet::new();
30 let mut files: Vec<(PathBuf, PathBuf)> = Vec::new();
31
32 for profile in profiles {
34 let mut stack = Vec::new();
35 resolve_profile_for_family(
36 profile,
37 cfg,
38 family,
39 &mut seen_files,
40 &mut stack,
41 &mut files,
42 )?;
43 }
44
45 if output.is_json() {
46 let pre_prompt_text = match pre_prompt {
50 Some(explicit) => explicit.to_string(),
51 None if framing.is_full() => default_pre_prompt(),
52 None => String::new(),
53 };
54
55 let system_info = if framing.is_full() {
56 let date = Local::now().format("%Y-%m-%d").to_string();
57 let os = env::consts::OS;
58 let arch = env::consts::ARCH;
59 format!("Today is {date}, and you are running on a {arch}/{os} system.")
60 } else {
61 String::new()
62 };
63
64 let mut fragments = Vec::new();
65 for (path, library_root) in &files {
66 let content = fs::read_to_string(path).map_err(|source| PrompterError::Io {
67 path: path.clone(),
68 source,
69 })?;
70 let rel_path = path
71 .strip_prefix(library_root)
72 .unwrap_or(path)
73 .display()
74 .to_string();
75 fragments.push(FragmentOutput {
76 path: rel_path,
77 content,
78 });
79 }
80
81 let payload = serde_json::to_value(RenderOutput {
82 profile: profiles.join(", "),
83 pre_prompt: pre_prompt_text,
84 system_info,
85 fragments,
86 })?;
87 writeln!(
88 &mut w,
89 "{}",
90 render_response("run", JsonOutput::Json, payload, String::new())
91 )
92 .map_err(PrompterError::Write)?;
93 } else {
94 let default_pre = default_pre_prompt();
99 let pre_prompt_text = match (pre_prompt, framing) {
100 (Some(explicit), _) => Some(explicit),
101 (None, Framing::Full) => Some(default_pre.as_str()),
102 (None, Framing::Bare) => None,
103 };
104 if let Some(text) = pre_prompt_text {
105 w.write_all(text.as_bytes()).map_err(PrompterError::Write)?;
106 }
107
108 if framing.is_full() {
111 w.write_all(b"\n").map_err(PrompterError::Write)?;
112 let prefix = format_system_prefix();
113 w.write_all(prefix.as_bytes())
114 .map_err(PrompterError::Write)?;
115 }
116
117 let sep = separator.unwrap_or("");
118 for (index, (path, _library_root)) in files.iter().enumerate() {
119 if framing.is_full() || index > 0 {
122 w.write_all(b"\n").map_err(PrompterError::Write)?;
123 }
124
125 let bytes = fs::read(path).map_err(|source| PrompterError::Io {
126 path: path.clone(),
127 source,
128 })?;
129 w.write_all(&bytes).map_err(PrompterError::Write)?;
130
131 if !sep.is_empty() {
133 w.write_all(sep.as_bytes()).map_err(PrompterError::Write)?;
134 }
135 }
136
137 let default_post = default_post_prompt();
142 let post_prompt_text = match framing {
143 Framing::Full => Some(
144 post_prompt
145 .or(cfg.post_prompt.as_deref())
146 .unwrap_or(&default_post),
147 ),
148 Framing::Bare => post_prompt,
149 };
150 if let Some(text) = post_prompt_text {
151 if framing.is_full() {
152 w.write_all(b"\n\n").map_err(PrompterError::Write)?;
153 }
154 w.write_all(text.as_bytes()).map_err(PrompterError::Write)?;
155 }
156 }
157
158 Ok(())
159}
160
161pub fn run_render_stdout(
183 profiles: &[String],
184 family: Option<&FamilyName>,
185 separator: Option<&str>,
186 pre_prompt: Option<&str>,
187 post_prompt: Option<&str>,
188 framing: Framing,
189 config_override: Option<&Path>,
190 output: JsonOutput,
191) -> Result<(), PrompterError> {
192 let (_cfg_path, cfg) = load_bundle(config_override)?;
193 let stdout = io::stdout();
194 let handle = stdout.lock();
195 render_to_writer(
196 &cfg,
197 handle,
198 profiles,
199 family,
200 separator,
201 pre_prompt,
202 post_prompt,
203 framing,
204 output,
205 )
206}
207
208pub fn render_to_vec(
222 profiles: &[String],
223 family: Option<&FamilyName>,
224 config_override: Option<&Path>,
225) -> Result<Vec<u8>, PrompterError> {
226 let (_cfg_path, cfg) = load_bundle(config_override)?;
227 let mut buf = Vec::new();
228 render_to_writer(
229 &cfg,
230 &mut buf,
231 profiles,
232 family,
233 None,
234 None,
235 None,
236 Framing::Full,
237 JsonOutput::Text,
238 )?;
239 Ok(buf)
240}