aiward 0.5.15

Local-first AI secret firewall for development environments.
Documentation
use std::time::Duration;

use indicatif::{ProgressBar, ProgressStyle};

const SPINNER_FRAMES: &str = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
const TICK_MS: u64 = 80;
const PAD: &str = "  ";

pub fn header(project: &str) {
    eprintln!();
    eprintln!("{}◆ ward  ·  {}", PAD, project);
    eprintln!();
}

pub fn header_cmd(cmd: &str, project: &str) {
    eprintln!();
    eprintln!("{}◆ ward {}  ·  {}", PAD, cmd, project);
    eprintln!();
}

pub fn guided_header(command: &str, project: &str, path: &std::path::Path, body: &str) {
    eprint!("{}", render_guided_header(command, project, path, body));
}

pub fn guided_context_header(
    command: &str,
    label: &str,
    name: &str,
    path: &std::path::Path,
    body: &str,
) {
    eprint!(
        "{}",
        render_guided_context_header(command, label, name, path, body)
    );
}

pub fn render_guided_header(
    command: &str,
    project: &str,
    path: &std::path::Path,
    body: &str,
) -> String {
    render_guided_context_header(command, "Project", project, path, body)
}

pub fn render_guided_context_header(
    command: &str,
    label: &str,
    name: &str,
    path: &std::path::Path,
    body: &str,
) -> String {
    format!(
        "\n{PAD}◬ ward {command}\n{PAD}{label}: {name}\n{PAD}Path: {}\n\n{PAD}{body}\n\n",
        short_path(path)
    )
}

pub fn section(label: &str) {
    eprintln!();
    eprintln!("{}  {}", PAD, label);
}

pub fn spinner(msg: &str) -> ProgressBar {
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::with_template(&format!("{}{{spinner:.cyan}} {{msg}}", PAD))
            .unwrap()
            .tick_chars(SPINNER_FRAMES),
    );
    pb.set_message(msg.to_string());
    pb.enable_steady_tick(Duration::from_millis(TICK_MS));
    pb
}

pub fn done(pb: ProgressBar, msg: &str) {
    pb.finish_and_clear();
    eprintln!("{}{}", PAD, msg);
}

pub fn warn_step(pb: ProgressBar, msg: &str) {
    pb.finish_and_clear();
    eprintln!("{}! {}", PAD, msg);
}

pub fn ok(msg: &str) {
    eprintln!("{}{}", PAD, msg);
}

pub fn fail(msg: &str) {
    eprintln!("{}{}", PAD, msg);
}

pub fn info(msg: &str) {
    eprintln!("{}  {}", PAD, msg);
}

pub fn warn(msg: &str) {
    eprintln!("{}! {}", PAD, msg);
}

pub fn blank() {
    eprintln!();
}

pub fn next(msg: &str) {
    eprintln!("{}{}", PAD, msg);
}

pub fn command_hint(command: &str) {
    eprintln!("{}  {}", PAD, command);
}

/// Shorten an absolute path for display — keep last 2 segments.
pub fn short_path(p: &std::path::Path) -> String {
    let parts: Vec<_> = p.components().collect();
    if parts.len() <= 3 {
        return p.display().to_string();
    }
    let tail: std::path::PathBuf = parts[parts.len() - 2..].iter().collect();
    format!("…/{}", tail.display())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn short_path_keeps_tail_segments() {
        assert_eq!(
            short_path(std::path::Path::new("/Users/me/project")),
            "…/me/project"
        );
    }

    #[test]
    fn guided_header_includes_command_project_path_and_body() {
        let rendered = render_guided_header(
            "setup",
            "demo",
            std::path::Path::new("/Users/me/demo"),
            "Ward will encrypt your local env, create a vault, and prepare this project for safe human and agent access.",
        );

        assert!(rendered.contains("◬ ward setup"));
        assert!(rendered.contains("Project: demo"));
        assert!(rendered.contains("Path: …/me/demo"));
        assert!(rendered.contains("encrypt your local env"));
    }

    #[test]
    fn guided_context_header_supports_workspace_label() {
        let rendered = render_guided_context_header(
            "setup",
            "Workspace",
            "cms-core",
            std::path::Path::new("/Users/me/cms-core"),
            "Ward detected a monorepo workspace.",
        );

        assert!(rendered.contains("◬ ward setup"));
        assert!(rendered.contains("Workspace: cms-core"));
        assert!(rendered.contains("Path: …/me/cms-core"));
    }

    #[test]
    fn guided_copy_fragments_are_stable() {
        let setup = "Ward will encrypt your local env, create a vault, and prepare this project for safe human and agent access.";
        let human = "This terminal is now protected. Normal commands in this Ward project will receive vault envs through Ward while this session is active.";

        assert!(setup.contains("encrypt your local env"));
        assert!(setup.contains("safe human and agent access"));
        assert!(human.contains("This terminal is now protected"));
        assert!(human.contains("while this session is active"));
    }
}