use anyhow::{bail, Result};
use std::io::IsTerminal;
#[derive(Clone)]
pub struct PatternEntry {
pub category: &'static str,
pub tool: &'static str,
pub savings: &'static str,
pub sample: &'static str,
}
pub static ALL_PATTERNS: &[PatternEntry] = &[
PatternEntry {
category: "vcs",
tool: "git",
savings: "80–95%",
sample: "git log --oneline -20",
},
PatternEntry {
category: "vcs",
tool: "gh",
savings: "70–85%",
sample: "gh pr list",
},
PatternEntry {
category: "vcs",
tool: "jj",
savings: "75–90%",
sample: "jj log",
},
PatternEntry {
category: "build",
tool: "cargo",
savings: "65–90%",
sample: "cargo build --release",
},
PatternEntry {
category: "build",
tool: "go",
savings: "60–85%",
sample: "go build ./...",
},
PatternEntry {
category: "build",
tool: "make",
savings: "55–80%",
sample: "make all",
},
PatternEntry {
category: "build",
tool: "gradle",
savings: "60–85%",
sample: "gradle build",
},
PatternEntry {
category: "build",
tool: "bazel",
savings: "65–85%",
sample: "bazel build //...",
},
PatternEntry {
category: "build",
tool: "nx",
savings: "60–80%",
sample: "nx build my-app",
},
PatternEntry {
category: "build",
tool: "vite",
savings: "55–75%",
sample: "vite build",
},
PatternEntry {
category: "build",
tool: "turbo",
savings: "60–80%",
sample: "turbo build",
},
PatternEntry {
category: "build",
tool: "zig",
savings: "60–85%",
sample: "zig build",
},
PatternEntry {
category: "build",
tool: "flutter",
savings: "55–80%",
sample: "flutter build apk",
},
PatternEntry {
category: "build",
tool: "swift",
savings: "60–80%",
sample: "swift build",
},
PatternEntry {
category: "build",
tool: "dotnet",
savings: "60–80%",
sample: "dotnet build",
},
PatternEntry {
category: "build",
tool: "buf",
savings: "55–75%",
sample: "buf generate",
},
PatternEntry {
category: "build",
tool: "rustfmt",
savings: "70–90%",
sample: "cargo fmt --check",
},
PatternEntry {
category: "test",
tool: "jest",
savings: "70–90%",
sample: "jest --testPathPattern auth",
},
PatternEntry {
category: "test",
tool: "pytest",
savings: "70–90%",
sample: "pytest tests/ -v",
},
PatternEntry {
category: "test",
tool: "vitest",
savings: "70–90%",
sample: "vitest run",
},
PatternEntry {
category: "test",
tool: "playwright",
savings: "75–90%",
sample: "playwright test",
},
PatternEntry {
category: "test",
tool: "rspec",
savings: "70–85%",
sample: "rspec spec/",
},
PatternEntry {
category: "lint",
tool: "clippy",
savings: "55–80%",
sample: "cargo clippy -- -D warnings",
},
PatternEntry {
category: "lint",
tool: "eslint",
savings: "55–80%",
sample: "eslint src/",
},
PatternEntry {
category: "lint",
tool: "ruff",
savings: "60–85%",
sample: "ruff check src/",
},
PatternEntry {
category: "lint",
tool: "mypy",
savings: "55–75%",
sample: "mypy src/",
},
PatternEntry {
category: "lint",
tool: "golangci",
savings: "55–80%",
sample: "golangci-lint run",
},
PatternEntry {
category: "lint",
tool: "biome",
savings: "55–80%",
sample: "biome check src/",
},
PatternEntry {
category: "lint",
tool: "pylint",
savings: "55–75%",
sample: "pylint src/",
},
PatternEntry {
category: "lint",
tool: "flake8",
savings: "55–75%",
sample: "flake8 src/",
},
PatternEntry {
category: "lint",
tool: "trivy",
savings: "50–75%",
sample: "trivy image myapp:latest",
},
PatternEntry {
category: "lint",
tool: "semgrep",
savings: "55–80%",
sample: "semgrep --config auto",
},
PatternEntry {
category: "lint",
tool: "hadolint",
savings: "50–70%",
sample: "hadolint Dockerfile",
},
PatternEntry {
category: "lint",
tool: "stylelint",
savings: "50–70%",
sample: "stylelint '**/*.css'",
},
PatternEntry {
category: "lint",
tool: "grype",
savings: "55–75%",
sample: "grype dir:.",
},
PatternEntry {
category: "lint",
tool: "swiftlint",
savings: "50–70%",
sample: "swiftlint lint",
},
PatternEntry {
category: "lint",
tool: "rubocop",
savings: "50–70%",
sample: "rubocop lib/",
},
PatternEntry {
category: "pkg",
tool: "npm",
savings: "60–80%",
sample: "npm install",
},
PatternEntry {
category: "pkg",
tool: "pnpm",
savings: "60–80%",
sample: "pnpm install",
},
PatternEntry {
category: "pkg",
tool: "yarn",
savings: "60–80%",
sample: "yarn install",
},
PatternEntry {
category: "pkg",
tool: "bun",
savings: "60–80%",
sample: "bun install",
},
PatternEntry {
category: "pkg",
tool: "pip",
savings: "60–80%",
sample: "pip install -r requirements.txt",
},
PatternEntry {
category: "pkg",
tool: "poetry",
savings: "60–80%",
sample: "poetry install",
},
PatternEntry {
category: "pkg",
tool: "uv",
savings: "65–80%",
sample: "uv sync",
},
PatternEntry {
category: "pkg",
tool: "deno",
savings: "55–75%",
sample: "deno cache deps.ts",
},
PatternEntry {
category: "pkg",
tool: "gem",
savings: "55–75%",
sample: "bundle install",
},
PatternEntry {
category: "pkg",
tool: "mix",
savings: "55–75%",
sample: "mix deps.get",
},
PatternEntry {
category: "infra",
tool: "kubectl",
savings: "60–85%",
sample: "kubectl get pods -n production",
},
PatternEntry {
category: "infra",
tool: "docker",
savings: "60–85%",
sample: "docker build -t myapp .",
},
PatternEntry {
category: "infra",
tool: "helm",
savings: "60–80%",
sample: "helm upgrade myapp ./chart",
},
PatternEntry {
category: "infra",
tool: "terraform",
savings: "60–80%",
sample: "terraform plan",
},
PatternEntry {
category: "infra",
tool: "aws",
savings: "60–80%",
sample: "aws ecs describe-services",
},
PatternEntry {
category: "infra",
tool: "gcloud",
savings: "60–80%",
sample: "gcloud run services list",
},
PatternEntry {
category: "infra",
tool: "az",
savings: "60–80%",
sample: "az aks get-credentials",
},
PatternEntry {
category: "infra",
tool: "pulumi",
savings: "60–80%",
sample: "pulumi up",
},
PatternEntry {
category: "infra",
tool: "argocd",
savings: "60–80%",
sample: "argocd app list",
},
PatternEntry {
category: "infra",
tool: "fly",
savings: "55–75%",
sample: "fly deploy",
},
PatternEntry {
category: "infra",
tool: "ansible",
savings: "55–75%",
sample: "ansible-playbook site.yml",
},
PatternEntry {
category: "infra",
tool: "vercel",
savings: "55–75%",
sample: "vercel deploy",
},
PatternEntry {
category: "infra",
tool: "wrangler",
savings: "55–75%",
sample: "wrangler deploy",
},
PatternEntry {
category: "infra",
tool: "supabase",
savings: "55–75%",
sample: "supabase db push",
},
PatternEntry {
category: "db",
tool: "psql",
savings: "55–75%",
sample: "psql -c '\\dt'",
},
PatternEntry {
category: "db",
tool: "mysql",
savings: "55–75%",
sample: "mysql -e 'SHOW TABLES'",
},
PatternEntry {
category: "db",
tool: "prisma",
savings: "60–80%",
sample: "prisma migrate dev",
},
PatternEntry {
category: "db",
tool: "redis",
savings: "50–70%",
sample: "redis-cli info",
},
PatternEntry {
category: "db",
tool: "mongosh",
savings: "50–70%",
sample: "mongosh --eval 'db.stats()'",
},
PatternEntry {
category: "ai",
tool: "mlflow",
savings: "50–70%",
sample: "mlflow experiments list",
},
PatternEntry {
category: "ai",
tool: "ollama",
savings: "50–70%",
sample: "ollama list",
},
PatternEntry {
category: "ai",
tool: "dbt",
savings: "55–75%",
sample: "dbt run",
},
PatternEntry {
category: "ai",
tool: "spark",
savings: "55–75%",
sample: "spark-submit job.py",
},
PatternEntry {
category: "ai",
tool: "alembic",
savings: "55–75%",
sample: "alembic upgrade head",
},
PatternEntry {
category: "ai",
tool: "flyway",
savings: "55–75%",
sample: "flyway migrate",
},
PatternEntry {
category: "sys",
tool: "curl",
savings: "50–75%",
sample: "curl -I https://api.example.com/health",
},
PatternEntry {
category: "sys",
tool: "jq",
savings: "50–70%",
sample: "curl ... | jq '.'",
},
PatternEntry {
category: "sys",
tool: "env",
savings: "50–70%",
sample: "env | grep AWS",
},
PatternEntry {
category: "sys",
tool: "systemd",
savings: "55–75%",
sample: "systemctl status myservice",
},
PatternEntry {
category: "sys",
tool: "mise",
savings: "50–70%",
sample: "mise list",
},
];
pub const CATEGORIES: &[(&str, &str)] = &[
("vcs", "Version control — git, gh, jj"),
(
"build",
"Build tools — cargo, go, make, gradle, bazel, zig, …",
),
(
"test",
"Test runners — jest, pytest, vitest, playwright, rspec",
),
(
"lint",
"Linters & scanners — clippy, eslint, ruff, trivy, semgrep, …",
),
(
"pkg",
"Package managers — npm, pnpm, pip, poetry, uv, gem, …",
),
(
"infra",
"Infrastructure — kubectl, docker, helm, terraform, aws, …",
),
("db", "Databases — psql, mysql, prisma, redis, mongosh"),
(
"ai",
"AI / Data — mlflow, ollama, dbt, spark, alembic, flyway",
),
("sys", "System utilities — curl, jq, env, systemd, mise"),
];
pub fn handle(json: bool, category: Option<String>) -> Result<()> {
let filtered = match &category {
Some(cat) => {
let cat_lower = cat.to_lowercase();
if !CATEGORIES.iter().any(|(k, _)| *k == cat_lower.as_str()) {
let valid: Vec<&str> = CATEGORIES.iter().map(|(k, _)| *k).collect();
bail!(
"unknown category '{}' — valid categories: {}",
cat,
valid.join(", ")
);
}
ALL_PATTERNS
.iter()
.filter(|p| p.category == cat_lower.as_str())
.collect::<Vec<_>>()
}
None => ALL_PATTERNS.iter().collect(),
};
if json {
return print_json(&filtered);
}
print_table(&filtered, category.as_deref());
Ok(())
}
fn print_table(patterns: &[&PatternEntry], category_filter: Option<&str>) {
let is_tty = std::io::stdout().is_terminal();
let sep = " ─────────────────────────────────────────────────────────────────────────";
println!();
let header = if let Some(cat) = category_filter {
format!("bctx patterns — category: {cat}")
} else {
format!(
"bctx domain patterns — {} patterns across {} categories",
patterns.len(),
CATEGORIES.len()
)
};
if is_tty {
println!(" \x1b[1m{header}\x1b[0m");
} else {
println!(" {header}");
}
println!("{sep}");
println!(
" {:<10} {:<18} {:<12} SAMPLE COMMAND",
"CATEGORY", "TOOL", "SAVINGS"
);
println!("{sep}");
let mut last_cat = "";
for p in patterns {
let cat_col = if p.category != last_cat {
last_cat = p.category;
if is_tty {
format!("\x1b[33m{:<10}\x1b[0m", p.category)
} else {
format!("{:<10}", p.category)
}
} else {
format!("{:<10}", "")
};
let tool_col = if is_tty {
format!("\x1b[36m{:<18}\x1b[0m", p.tool)
} else {
format!("{:<18}", p.tool)
};
println!(
" {} {} {:<12} {}",
cat_col, tool_col, p.savings, p.sample
);
}
println!("{sep}");
println!();
if category_filter.is_none() {
println!(" Filter by category: bctx patterns --category vcs");
println!(" Machine-readable: bctx patterns --json");
println!();
println!(" Categories:");
for (key, desc) in CATEGORIES {
if is_tty {
println!(" \x1b[33m{:<8}\x1b[0m {}", key, desc);
} else {
println!(" {:<8} {}", key, desc);
}
}
println!();
}
}
fn print_json(patterns: &[&PatternEntry]) -> Result<()> {
let arr: Vec<serde_json::Value> = patterns
.iter()
.map(|p| {
serde_json::json!({
"category": p.category,
"tool": p.tool,
"savings": p.savings,
"sample": p.sample,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&arr)?);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_patterns_have_non_empty_fields() {
for p in ALL_PATTERNS {
assert!(
!p.category.is_empty(),
"empty category for tool '{}'",
p.tool
);
assert!(!p.tool.is_empty(), "empty tool name");
assert!(!p.savings.is_empty(), "empty savings for tool '{}'", p.tool);
assert!(!p.sample.is_empty(), "empty sample for tool '{}'", p.tool);
}
}
#[test]
fn all_patterns_use_known_categories() {
let valid: Vec<&str> = CATEGORIES.iter().map(|(k, _)| *k).collect();
for p in ALL_PATTERNS {
assert!(
valid.contains(&p.category),
"unknown category '{}' for tool '{}'",
p.category,
p.tool
);
}
}
#[test]
fn categories_list_has_nine_entries() {
assert_eq!(CATEGORIES.len(), 9);
}
#[test]
fn categories_have_unique_keys() {
let keys: Vec<&str> = CATEGORIES.iter().map(|(k, _)| *k).collect();
let unique: std::collections::HashSet<_> = keys.iter().collect();
assert_eq!(
keys.len(),
unique.len(),
"duplicate category keys: {keys:?}"
);
}
#[test]
fn total_pattern_count_exceeds_seventy() {
assert!(
ALL_PATTERNS.len() > 70,
"expected > 70 patterns, got {}",
ALL_PATTERNS.len()
);
}
#[test]
fn filter_vcs_returns_only_vcs_entries() {
let vcs: Vec<_> = ALL_PATTERNS
.iter()
.filter(|p| p.category == "vcs")
.collect();
assert!(!vcs.is_empty(), "no vcs patterns found");
for p in &vcs {
assert_eq!(p.category, "vcs");
}
}
#[test]
fn filter_unknown_category_returns_error() {
let result = handle(false, Some("unknowncategory".to_string()));
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("unknown category"), "unexpected error: {msg}");
assert!(
msg.contains("valid categories"),
"missing category list: {msg}"
);
}
#[test]
fn handle_table_all_succeeds() {
assert!(handle(false, None).is_ok());
}
#[test]
fn handle_table_filtered_succeeds() {
assert!(handle(false, Some("build".to_string())).is_ok());
}
#[test]
fn handle_json_all_succeeds() {
assert!(handle(true, None).is_ok());
}
#[test]
fn handle_json_filtered_succeeds() {
assert!(handle(true, Some("infra".to_string())).is_ok());
}
#[test]
fn handle_category_lookup_is_case_insensitive() {
assert!(handle(false, Some("VCS".to_string())).is_ok());
assert!(handle(true, Some("Build".to_string())).is_ok());
}
#[test]
fn json_flag_produces_valid_json_for_all() {
let patterns: Vec<_> = ALL_PATTERNS.iter().collect();
let arr: Vec<serde_json::Value> = patterns
.iter()
.map(|p| serde_json::json!({"category": p.category, "tool": p.tool}))
.collect();
let text = serde_json::to_string_pretty(&arr).expect("serialise");
let parsed: Vec<serde_json::Value> = serde_json::from_str(&text).expect("parse");
assert_eq!(parsed.len(), ALL_PATTERNS.len());
}
#[test]
fn json_flag_produces_valid_json_for_category() {
let build: Vec<_> = ALL_PATTERNS
.iter()
.filter(|p| p.category == "build")
.collect();
let arr: Vec<serde_json::Value> = build
.iter()
.map(|p| serde_json::json!({"tool": p.tool, "savings": p.savings}))
.collect();
let text = serde_json::to_string_pretty(&arr).expect("serialise");
let _parsed: Vec<serde_json::Value> = serde_json::from_str(&text).expect("parse");
}
#[test]
fn no_duplicate_tool_names_within_same_category() {
for (cat, _) in CATEGORIES {
let tools: Vec<_> = ALL_PATTERNS
.iter()
.filter(|p| p.category == *cat)
.map(|p| p.tool)
.collect();
let unique: std::collections::HashSet<_> = tools.iter().collect();
assert_eq!(
tools.len(),
unique.len(),
"duplicate tool in category '{cat}': {tools:?}"
);
}
}
}