tftio-prompter 2.6.0

A CLI tool for composing reusable prompt snippets from a library using TOML profiles
Documentation
//! Profile rendering and output formatting.
use crate::config::Config;
use crate::config::load_bundle;
use crate::{
    FragmentOutput, PrompterError, RenderOutput, default_post_prompt, default_pre_prompt,
    format_system_prefix, resolve_profile,
};
use chrono::Local;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use tftio_cli_common::{JsonOutput, render_response};

/// Render one or more profiles to the given writer.
pub fn render_to_writer(
    cfg: &Config,
    mut w: impl Write,
    profiles: &[String],
    separator: Option<&str>,
    pre_prompt: Option<&str>,
    post_prompt: Option<&str>,
    output: JsonOutput,
) -> Result<(), PrompterError> {
    let mut seen_files = HashSet::new();
    let mut files: Vec<(PathBuf, PathBuf)> = Vec::new();

    // Resolve all profiles with shared deduplication
    for profile in profiles {
        let mut stack = Vec::new();
        resolve_profile(profile, cfg, &mut seen_files, &mut stack, &mut files)?;
    }

    if output.is_json() {
        // JSON output mode
        let default_pre = default_pre_prompt();
        let pre_prompt_text = pre_prompt.unwrap_or(&default_pre).to_string();

        let date = Local::now().format("%Y-%m-%d").to_string();
        let os = env::consts::OS;
        let arch = env::consts::ARCH;
        let system_info = format!("Today is {date}, and you are running on a {arch}/{os} system.");

        let mut fragments = Vec::new();
        for (path, library_root) in &files {
            let content = fs::read_to_string(path).map_err(|source| PrompterError::Io {
                path: path.clone(),
                source,
            })?;
            let rel_path = path
                .strip_prefix(library_root)
                .unwrap_or(path)
                .display()
                .to_string();
            fragments.push(FragmentOutput {
                path: rel_path,
                content,
            });
        }

        let payload = serde_json::to_value(RenderOutput {
            profile: profiles.join(", "),
            pre_prompt: pre_prompt_text,
            system_info,
            fragments,
        })?;
        writeln!(
            &mut w,
            "{}",
            render_response("run", JsonOutput::Json, payload, String::new())
        )
        .map_err(PrompterError::Write)?;
    } else {
        // Text output mode
        // Write pre-prompt (defaults if not provided)
        let default_pre = default_pre_prompt();
        let pre_prompt_text = pre_prompt.unwrap_or(&default_pre);
        w.write_all(pre_prompt_text.as_bytes())
            .map_err(PrompterError::Write)?;

        // Write system prefix with two newlines before
        w.write_all(b"\n").map_err(PrompterError::Write)?;
        let prefix = format_system_prefix();
        w.write_all(prefix.as_bytes())
            .map_err(PrompterError::Write)?;

        let sep = separator.unwrap_or("");
        for (path, _library_root) in files {
            // Two newlines before each file
            w.write_all(b"\n").map_err(PrompterError::Write)?;

            let bytes = fs::read(&path).map_err(|source| PrompterError::Io {
                path: path.clone(),
                source,
            })?;
            w.write_all(&bytes).map_err(PrompterError::Write)?;

            // Write separator after each file if provided
            if !sep.is_empty() {
                w.write_all(sep.as_bytes()).map_err(PrompterError::Write)?;
            }
        }

        // Write post-prompt (defaults if not provided)
        let default_post = default_post_prompt();
        let post_prompt_text = post_prompt
            .or(cfg.post_prompt.as_deref())
            .unwrap_or(&default_post);

        // Two newlines before post-prompt
        w.write_all(b"\n\n").map_err(PrompterError::Write)?;
        w.write_all(post_prompt_text.as_bytes())
            .map_err(PrompterError::Write)?;
    }

    Ok(())
}

/// Render one or more profiles to stdout.
///
/// Convenience function that reads configuration and renders the specified
/// profiles to standard output with optional separator, pre-prompt, and post-prompt.
/// When multiple profiles are provided, files are deduplicated across all profiles.
///
/// # Arguments
/// * `profiles` - Profile names to render (deduplicated in order)
/// * `separator` - Optional separator between files
/// * `pre_prompt` - Optional custom pre-prompt text
/// * `post_prompt` - Optional custom post-prompt text
/// * `config_override` - Optional configuration file override
/// * `json` - Whether to output in JSON format
///
/// # Errors
/// Returns an error if:
/// - Configuration file cannot be read or parsed
/// - Profile resolution fails
/// - Writing to stdout fails
pub fn run_render_stdout(
    profiles: &[String],
    separator: Option<&str>,
    pre_prompt: Option<&str>,
    post_prompt: Option<&str>,
    config_override: Option<&Path>,
    output: JsonOutput,
) -> Result<(), PrompterError> {
    let (_cfg_path, cfg) = load_bundle(config_override)?;
    let stdout = io::stdout();
    let handle = stdout.lock();
    render_to_writer(
        &cfg,
        handle,
        profiles,
        separator,
        pre_prompt,
        post_prompt,
        output,
    )
}

/// Render composed profiles to a byte vector.
///
/// Convenience wrapper around [`render_to_writer`] that handles config
/// resolution and returns the rendered output as bytes. Intended for
/// use by other crates that need prompt composition as a library.
///
/// # Arguments
/// * `profiles` - Profile names to compose
/// * `config_override` - Optional path to custom config file
///
/// # Errors
/// Returns an error if config resolution, profile resolution, or rendering fails.
pub fn render_to_vec(
    profiles: &[String],
    config_override: Option<&Path>,
) -> Result<Vec<u8>, PrompterError> {
    let (_cfg_path, cfg) = load_bundle(config_override)?;
    let mut buf = Vec::new();
    render_to_writer(&cfg, &mut buf, profiles, None, None, None, JsonOutput::Text)?;
    Ok(buf)
}