tokf 0.2.33

Config-driven CLI tool that compresses command output before it reaches an LLM context
Documentation
use std::io::BufRead as _;

use tokf::auth::credentials;
use tokf::config;
use tokf::publish_shared::{collect_test_files_resolved, hash_filter, inline_lua_script};
use tokf::remote::http::Client;
use tokf::remote::publish_client;

/// Entry point for the `tokf publish` subcommand.
pub fn cmd_publish(filter_name: &str, dry_run: bool, update_tests: bool) -> i32 {
    let result = if update_tests {
        publish_update_tests(filter_name, dry_run)
    } else {
        publish(filter_name, dry_run)
    };
    match result {
        Ok(code) => code,
        Err(e) => {
            eprintln!("[tokf] error: {e:#}");
            1
        }
    }
}

// ── Shared helpers ──────────────────────────────────────────────────────────

/// Resolve a local filter by name, rejecting stdlib filters.
fn resolve_local_filter(filter_name: &str) -> anyhow::Result<config::ResolvedFilter> {
    let search_dirs = config::default_search_dirs();
    let resolved = config::discover_all_filters(&search_dirs)?;
    let resolved_filter = resolved
        .into_iter()
        .find(|f| f.matches_name(filter_name))
        .ok_or_else(|| anyhow::anyhow!("filter not found: {filter_name}"))?;

    if resolved_filter.priority == tokf::config::STDLIB_PRIORITY {
        anyhow::bail!(
            "'{filter_name}' is a built-in stdlib filter — \
             eject it first with `tokf eject {filter_name}`"
        );
    }

    Ok(resolved_filter)
}

// ── Publish flow ────────────────────────────────────────────────────────────

fn publish(filter_name: &str, dry_run: bool) -> anyhow::Result<i32> {
    let filter_name = filter_name.strip_suffix(".toml").unwrap_or(filter_name);
    let resolved_filter = resolve_local_filter(filter_name)?;

    let filter_bytes = std::fs::read(&resolved_filter.source_path)?;
    let filter_bytes = inline_lua_script(filter_bytes, &resolved_filter.source_path)?;
    let (content_hash, command_pattern) = hash_filter(&filter_bytes)?;
    let test_files = collect_test_files_resolved(&resolved_filter.source_path)?;

    eprintln!("[tokf] publishing filter: {filter_name}");
    eprintln!("  Command: {command_pattern}");
    eprintln!("  Hash:    {content_hash}");
    eprintln!("  Tests:   {} file(s)", test_files.len());

    if dry_run {
        eprintln!("[tokf] dry-run: no files uploaded");
        return Ok(0);
    }

    ensure_license_accepted()?;
    let client = Client::authed()?;

    let (is_new, resp) = tokf::remote::retry::with_retry("publish", || {
        publish_client::publish_filter(&client, &filter_bytes, &test_files)
    })?;

    if is_new {
        eprintln!("[tokf] published {filter_name}  (201 Created)");
    } else {
        eprintln!("[tokf] already exists  (200 OK)");
    }
    eprintln!("Hash:    {}", resp.content_hash);
    eprintln!("Author:  {}", resp.author);
    eprintln!("URL:     {}", resp.registry_url);
    Ok(0)
}

// ── Update-tests flow ───────────────────────────────────────────────────────

fn publish_update_tests(filter_name: &str, dry_run: bool) -> anyhow::Result<i32> {
    let filter_name = filter_name.strip_suffix(".toml").unwrap_or(filter_name);
    let resolved_filter = resolve_local_filter(filter_name)?;

    let filter_bytes = std::fs::read(&resolved_filter.source_path)?;
    let (content_hash, _) = hash_filter(&filter_bytes)?;
    let test_files = collect_test_files_resolved(&resolved_filter.source_path)?;

    if test_files.is_empty() {
        anyhow::bail!("no test files found for {filter_name}");
    }

    // Validate test files locally before uploading
    for (filename, bytes) in &test_files {
        tokf_common::test_case::validate(bytes).map_err(|e| anyhow::anyhow!("{filename}: {e}"))?;
    }

    eprintln!("[tokf] updating test suite for: {filter_name}");
    eprintln!("  Hash:  {content_hash}");
    eprintln!("  Tests: {} file(s)", test_files.len());

    if dry_run {
        for (name, _) in &test_files {
            eprintln!("  - {name}");
        }
        eprintln!("[tokf] dry-run: no files uploaded");
        return Ok(0);
    }

    let client = Client::authed()?;

    let resp = tokf::remote::retry::with_retry("update-tests", || {
        publish_client::update_tests(&client, &content_hash, &test_files)
    })?;

    eprintln!(
        "[tokf] updated test suite for {filter_name} ({} file(s))",
        resp.test_count
    );
    eprintln!("URL: {}", resp.registry_url);
    Ok(0)
}

fn ensure_license_accepted() -> anyhow::Result<()> {
    let accepted = credentials::load()
        .and_then(|a| a.mit_license_accepted)
        .unwrap_or(false);

    if accepted {
        return Ok(());
    }

    eprintln!("[tokf] This filter will be published under the MIT license.");
    eprintln!("[tokf] Anyone may use, modify, and distribute it with attribution.");
    eprint!("[tokf] Accept MIT license? [y/N]: ");

    let mut input = String::new();
    std::io::stdin()
        .lock()
        .read_line(&mut input)
        .map_err(|e| anyhow::anyhow!("could not read input: {e}"))?;

    if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
        credentials::save_license_accepted(true)?;
        eprintln!("[tokf] MIT license accepted.");
        Ok(())
    } else {
        anyhow::bail!("MIT license not accepted — publish cancelled")
    }
}

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

    #[test]
    fn resolve_fails_for_unknown_filter() {
        let search_dirs = config::default_search_dirs();
        let resolved = config::discover_all_filters(&search_dirs).unwrap();
        let found = resolved
            .iter()
            .find(|f| f.matches_name("nonexistent/xyz-abc-filter-99"));
        assert!(found.is_none(), "expected no match for nonexistent filter");
    }
}