bctx-weave 0.1.10

bctx-weave — FilterMesh lens pipeline, CLI interception, domain compression
Documentation
use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;

// Deployment "Running..." / "- Running..." progress lines
static RUNNING_RE: Lazy<Regex> =
    Lazy::new(|| Regex::new(r"(?m)^[\s-]*Running\.\.\.[^\n]*\n?").unwrap());
// Verbose provisioning state lines in deployment output
static PROVISIONING_RE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(
        r#"(?m)^\s*"provisioningState"\s*:\s*"(Running|Updating|Deleting|Accepted|Creating)",?\n?"#,
    )
    .unwrap()
});
// Noisy resource ID fields in JSON output
const NOISY_FIELDS: &[&str] = &[
    "\"etag\":",
    "\"_etag\":",
    "\"systemData\":",
    "\"managedBy\":",
    "\"resourceGroup\":", // redundant when you know the RG
];

// ── az deployment group create/what-if ────────────────────────────────────────

pub fn compress_deployment(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let s = RUNNING_RE.replace_all(&cleaned, "");
    let s = PROVISIONING_RE.replace_all(&s, "");
    // Keep lines that signal completion or errors
    let lines: Vec<&str> = s
        .lines()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty() && !NOISY_FIELDS.iter().any(|n| t.starts_with(n))
        })
        .collect();
    let result = lines.join("\n");
    if result.lines().count() > 50 {
        let all: Vec<&str> = result.lines().collect();
        return format!(
            "{}\n… [{} more lines] …",
            all[..50].join("\n"),
            all.len() - 50
        );
    }
    compactor::collapse_blanks(&result)
}

// ── az ad sp create-for-rbac (credential redaction) ──────────────────────────

pub fn compress_service_principal(raw: &str) -> String {
    use once_cell::sync::Lazy;
    use regex::Regex;
    static CLIENT_SECRET_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""password"\s*:\s*"([^"]{8})[^"]*""#).unwrap());
    static CLIENT_ID_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""appId"\s*:\s*"([^"]{8})[^"]*""#).unwrap());

    let cleaned = compactor::normalise(raw);
    let secret_preview = CLIENT_SECRET_RE
        .captures(&cleaned)
        .map(|c| format!("{}", &c[1]))
        .unwrap_or_else(|| "REDACTED".to_string());
    let client_id = CLIENT_ID_RE
        .captures(&cleaned)
        .map(|c| format!("{}", &c[1]))
        .unwrap_or_else(|| "unknown".to_string());

    // Return compact one-liner; the full secret is dangerous to log
    format!(
        "Service principal created.\n  appId (clientId): {client_id}\n  password (clientSecret): {secret_preview}  ← store securely, not shown again"
    )
}

// ── az account list / show ─────────────────────────────────────────────────────

pub fn compress_account(raw: &str) -> String {
    use once_cell::sync::Lazy;
    use regex::Regex;
    static NAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""name"\s*:\s*"([^"]+)""#).unwrap());
    static ID_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""id"\s*:\s*"([^"]+)""#).unwrap());
    static IS_DEFAULT_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""isDefault"\s*:\s*(true|false)"#).unwrap());

    let cleaned = compactor::normalise(raw);
    let names: Vec<&str> = NAME_RE
        .captures_iter(&cleaned)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();
    let ids: Vec<&str> = ID_RE
        .captures_iter(&cleaned)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .filter(|s| s.contains('-')) // subscription IDs are UUIDs
        .collect();
    let defaults: Vec<bool> = IS_DEFAULT_RE
        .captures_iter(&cleaned)
        .map(|c| c.get(1).is_some_and(|m| m.as_str() == "true"))
        .collect();

    if names.is_empty() {
        return compress_generic(raw);
    }

    let lines: Vec<String> = names
        .iter()
        .enumerate()
        .map(|(i, name)| {
            let id = ids.get(i).unwrap_or(&"");
            let marker = if defaults.get(i).copied().unwrap_or(false) {
                " *"
            } else {
                ""
            };
            format!("{name}  {id}{marker}")
        })
        .collect();
    lines.join("\n")
}

// ── az webapp / functionapp / containerapp ────────────────────────────────────

pub fn compress_webapp(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let useful: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            let t = l.trim();
            t.contains("defaultHostName")
                || t.contains("hostName")
                || t.contains("url")
                || t.contains("state\":")
                || t.contains("\"name\":")
                || t.contains("succeeded")
                || t.contains("Running")
                || t.contains("Succeeded")
                || t.contains("Failed")
                || t.contains("error")
                || t.contains("Error")
        })
        .collect();
    if useful.is_empty() {
        return compress_generic(raw);
    }
    useful.join("\n")
}

// ── az vm list / show ─────────────────────────────────────────────────────────

pub fn compress_vm(raw: &str) -> String {
    use once_cell::sync::Lazy;
    use regex::Regex;
    static VM_NAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""name"\s*:\s*"([^"]+)""#).unwrap());
    static POWER_RE: Lazy<Regex> =
        Lazy::new(|| Regex::new(r#""powerState"\s*:\s*"([^"]+)""#).unwrap());
    static SIZE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""vmSize"\s*:\s*"([^"]+)""#).unwrap());

    let cleaned = compactor::normalise(raw);
    let names: Vec<&str> = VM_NAME_RE
        .captures_iter(&cleaned)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();

    if names.is_empty() || names.len() == 1 {
        return compress_generic(raw);
    }

    let powers: Vec<&str> = POWER_RE
        .captures_iter(&cleaned)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();
    let sizes: Vec<&str> = SIZE_RE
        .captures_iter(&cleaned)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();

    let count = names.len();
    let lines: Vec<String> = names
        .iter()
        .enumerate()
        .map(|(i, name)| {
            let power = powers.get(i).unwrap_or(&"unknown");
            let size = sizes.get(i).unwrap_or(&"");
            format!("{name}  {power}  {size}")
        })
        .take(20)
        .collect();

    let mut result = lines.join("\n");
    if count > 20 {
        result.push_str(&format!("\n{} more VMs", count - 20));
    }
    result
}

// ── generic az (large JSON / unknown subcommand) ──────────────────────────────

pub fn compress_generic(raw: &str) -> String {
    let cleaned = compactor::normalise(raw);
    let lines: Vec<&str> = cleaned
        .lines()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty() && !NOISY_FIELDS.iter().any(|n| t.starts_with(n))
        })
        .collect();
    if lines.len() > 40 {
        return format!(
            "{}\n… [{} more lines — use --query to filter] …",
            lines[..40].join("\n"),
            lines.len() - 40
        );
    }
    lines.join("\n")
}

// ── top-level dispatcher ──────────────────────────────────────────────────────

pub fn compress_az(subcmd: &str, raw: &str) -> String {
    let sub = subcmd.trim();
    if sub.starts_with("deployment") {
        return compress_deployment(raw);
    }
    if sub.starts_with("ad sp create-for-rbac") || sub.contains("create-for-rbac") {
        return compress_service_principal(raw);
    }
    if sub.starts_with("account") {
        return compress_account(raw);
    }
    if sub.starts_with("webapp")
        || sub.starts_with("functionapp")
        || sub.starts_with("containerapp")
    {
        return compress_webapp(raw);
    }
    if sub.starts_with("vm") {
        return compress_vm(raw);
    }
    compress_generic(raw)
}

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

    #[test]
    fn deployment_strips_running_progress() {
        let raw = "- Running...\n- Running...\n{\n  \"id\": \"/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Resources/deployments/deploy1\",\n  \"name\": \"deploy1\",\n  \"provisioningState\": \"Succeeded\"\n}\n";
        let out = compress_deployment(raw);
        assert!(!out.contains("- Running..."), "{out}");
        assert!(out.contains("Succeeded"), "{out}");
    }

    #[test]
    fn service_principal_redacts_password() {
        let raw = r#"{"appId":"00000000-dead-beef-0000-000000000001","displayName":"my-sp","password":"SuperSecretPassword123!","tenant":"00000000-dead-beef-0000-000000000002"}"#;
        let out = compress_service_principal(raw);
        assert!(!out.contains("SuperSecretPassword123!"), "{out}");
        assert!(out.contains("clientSecret"), "{out}");
        assert!(out.contains("appId"), "{out}");
    }

    #[test]
    fn account_list_extracts_names() {
        let raw = r#"[{"id":"sub-abc-123","name":"Production","state":"Enabled","isDefault":true},{"id":"sub-def-456","name":"Development","state":"Enabled","isDefault":false}]"#;
        let out = compress_account(raw);
        assert!(out.contains("Production"), "{out}");
        assert!(out.contains("Development"), "{out}");
        assert!(out.contains('*'), "{out}"); // default marker
    }

    #[test]
    fn generic_strips_noisy_fields() {
        let raw = "{\n  \"name\": \"my-resource\",\n  \"etag\": \"W/\\\"abc123\\\"\",\n  \"systemData\": { \"createdBy\": \"user@example.com\" }\n}\n";
        let out = compress_generic(raw);
        assert!(!out.contains("\"etag\":"), "{out}");
        assert!(!out.contains("\"systemData\":"), "{out}");
        assert!(out.contains("my-resource"), "{out}");
    }

    #[test]
    fn generic_truncates_large_output() {
        let lines: Vec<String> = (0..50)
            .map(|i| format!("  \"key{i}\": \"value{i}\","))
            .collect();
        let out = compress_generic(&lines.join("\n"));
        assert!(out.contains("more lines"), "{out}");
    }
}