use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static RUNNING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^[\s-]*Running\.\.\.[^\n]*\n?").unwrap());
static PROVISIONING_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"(?m)^\s*"provisioningState"\s*:\s*"(Running|Updating|Deleting|Accepted|Creating)",?\n?"#,
)
.unwrap()
});
const NOISY_FIELDS: &[&str] = &[
"\"etag\":",
"\"_etag\":",
"\"systemData\":",
"\"managedBy\":",
"\"resourceGroup\":", ];
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, "");
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)
}
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());
format!(
"Service principal created.\n appId (clientId): {client_id}\n password (clientSecret): {secret_preview} ← store securely, not shown again"
)
}
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('-')) .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")
}
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")
}
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
}
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")
}
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}"); }
#[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}");
}
}