#![allow(clippy::collapsible_if, clippy::let_and_return)]
rust_i18n::i18n!("locales", fallback = "en");
mod json;
mod locale;
mod sarif;
#[cfg(feature = "telemetry")]
pub mod telemetry;
#[cfg(not(feature = "telemetry"))]
mod telemetry_stub;
mod watch;
#[cfg(not(feature = "telemetry"))]
use telemetry_stub as telemetry;
use agnix_core::{
ValidationResult, apply_fixes_with_options,
config::{LintConfig, TargetTool},
diagnostics::{Diagnostic, DiagnosticLevel, FixConfidenceTier},
eval::{EvalFormat, evaluate_manifest_file},
fixes::{FixApplyMode, FixApplyOptions},
generate_schema, validate_project,
};
use clap::{Parser, Subcommand, ValueEnum};
use colored::*;
use rust_i18n::t;
use similar::{ChangeTag, TextDiff};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::process;
use std::time::Instant;
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum OutputFormat {
#[default]
Text,
Json,
Sarif,
}
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum TargetArg {
#[default]
Generic,
#[value(name = "claude-code")]
ClaudeCode,
Cursor,
Codex,
Kiro,
}
impl From<TargetArg> for TargetTool {
fn from(arg: TargetArg) -> Self {
match arg {
TargetArg::Generic => TargetTool::Generic,
TargetArg::ClaudeCode => TargetTool::ClaudeCode,
TargetArg::Cursor => TargetTool::Cursor,
TargetArg::Codex => TargetTool::Codex,
TargetArg::Kiro => TargetTool::Kiro,
}
}
}
#[derive(Parser)]
#[command(name = "agnix")]
#[command(author, version, about, long_about = None)]
#[command(
about = "The nginx of agent configs",
long_about = "Validate agent specifications across Claude Code, Cursor, Codex, and beyond.\n\nValidates: Skills • MCP • Hooks • Memory • Plugins"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short, long)]
strict: bool,
#[arg(short, long, value_enum, default_value_t = TargetArg::Generic)]
target: TargetArg,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
#[arg(long, group = "fix_mode")]
fix: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, group = "fix_mode")]
fix_safe: bool,
#[arg(long, group = "fix_mode")]
fix_unsafe: bool,
#[arg(long)]
show_fixes: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
#[arg(short, long)]
watch: bool,
#[arg(long)]
locale: Option<String>,
#[arg(long)]
list_locales: bool,
#[arg(long)]
max_files: Option<usize>,
}
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum EvalOutputFormat {
#[default]
Markdown,
Json,
Csv,
}
impl From<EvalOutputFormat> for EvalFormat {
fn from(f: EvalOutputFormat) -> Self {
match f {
EvalOutputFormat::Markdown => EvalFormat::Markdown,
EvalOutputFormat::Json => EvalFormat::Json,
EvalOutputFormat::Csv => EvalFormat::Csv,
}
}
}
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum TelemetryAction {
#[default]
Status,
Enable,
Disable,
}
#[derive(Subcommand)]
enum Commands {
Validate {
#[arg(default_value = ".")]
path: PathBuf,
},
Init {
#[arg(default_value = ".agnix.toml")]
output: PathBuf,
},
Eval {
path: PathBuf,
#[arg(long, short, value_enum, default_value_t = EvalOutputFormat::Markdown)]
format: EvalOutputFormat,
#[arg(long)]
filter: Option<String>,
#[arg(long, short)]
verbose: bool,
},
Telemetry {
#[arg(value_enum, default_value_t = TelemetryAction::Status)]
action: TelemetryAction,
},
Schema {
#[arg(short, long)]
output: Option<PathBuf>,
},
}
fn main() {
let cli = Cli::parse();
if cli.list_locales {
locale::print_supported_locales();
return;
}
locale::init(cli.locale.as_deref(), None);
if cli.verbose && matches!(cli.format, OutputFormat::Text) {
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("agnix=debug,agnix_core=debug"));
tracing_subscriber::registry()
.with(
fmt::layer()
.with_target(true)
.with_level(true)
.with_writer(std::io::stderr),
)
.with(filter)
.init();
tracing::debug!("Verbose mode enabled");
}
if cli.watch {
let config_path = resolve_config_path(&cli.path, cli.config.as_ref());
let (config, _) = LintConfig::load_or_default(config_path.as_ref());
if cli.locale.is_none() {
if let Some(config_locale) = config.locale() {
locale::init(None, Some(config_locale));
}
}
}
let result = match &cli.command {
Some(Commands::Validate { path }) => validate_command(path, &cli),
Some(Commands::Init { output }) => init_command(output),
Some(Commands::Eval {
path,
format,
filter,
verbose,
}) => eval_command(path, *format, filter.as_deref(), *verbose),
Some(Commands::Telemetry { action }) => telemetry_command(*action),
Some(Commands::Schema { output }) => schema_command(output.as_ref()),
None => validate_command(&cli.path, &cli),
};
if let Err(e) = result {
eprintln!("{} {}", t!("cli.error_label").red().bold(), e);
process::exit(1);
}
}
fn count_errors_warnings(diagnostics: &[Diagnostic]) -> (usize, usize) {
let errors = diagnostics
.iter()
.filter(|d| d.level == DiagnosticLevel::Error)
.count();
let warnings = diagnostics
.iter()
.filter(|d| d.level == DiagnosticLevel::Warning)
.count();
(errors, warnings)
}
#[tracing::instrument(skip(cli), fields(path = %path.display()))]
fn validate_command(path: &Path, cli: &Cli) -> anyhow::Result<()> {
tracing::debug!("Starting validation");
if cli.watch {
if !matches!(cli.format, OutputFormat::Text) {
return Err(anyhow::anyhow!("{}", t!("cli.watch_error_text_only")));
}
let should_fix = cli.fix || cli.fix_safe || cli.fix_unsafe || cli.dry_run;
if should_fix {
return Err(anyhow::anyhow!("{}", t!("cli.watch_error_fix")));
}
let path = path.to_path_buf();
let path_for_watch = path.clone();
let strict = cli.strict;
let verbose = cli.verbose;
let target = cli.target;
let config_override = cli.config.clone();
return watch::watch_and_validate(&path_for_watch, move || {
run_single_validation(&path, strict, verbose, target, config_override.as_ref())
});
}
let config_path = resolve_config_path(path, cli.config.as_ref());
tracing::debug!(config_path = ?config_path, "Resolved config path");
let (mut config, config_warning) = LintConfig::load_or_default(config_path.as_ref());
if cli.locale.is_none() {
if let Some(config_locale) = config.locale() {
locale::init(None, Some(config_locale));
}
}
if let Some(warning) = config_warning {
eprintln!("{} {}", t!("cli.warning_label").yellow().bold(), warning);
eprintln!();
}
config.set_target(cli.target.into());
if matches!(cli.format, OutputFormat::Text) {
let config_warnings = config.validate();
if !config_warnings.is_empty() {
for warning in &config_warnings {
eprintln!(
"{} [{}] {}",
t!("cli.config_warning_label").yellow().bold(),
warning.field.dimmed(),
warning.message
);
if let Some(suggestion) = &warning.suggestion {
eprintln!(" {} {}", t!("cli.hint_label").cyan(), suggestion);
}
}
eprintln!();
}
}
if let Some(max_files) = cli.max_files {
if max_files == 0 {
eprintln!(
"{} --max-files=0 disables file count protection. This may allow DoS via large projects.",
"Warning:".yellow().bold()
);
config.set_max_files_to_validate(None);
} else if max_files > 1_000_000 {
eprintln!(
"{} --max-files={} is very high. Consider using the default (10,000) for better performance.",
"Warning:".yellow().bold(),
max_files
);
config.set_max_files_to_validate(Some(max_files));
} else {
config.set_max_files_to_validate(Some(max_files));
}
}
let should_fix = cli.fix || cli.fix_safe || cli.fix_unsafe || cli.dry_run;
if should_fix && !matches!(cli.format, OutputFormat::Text) {
return Err(anyhow::anyhow!("{}", t!("cli.fix_error_text_only")));
}
let base_path = if matches!(cli.format, OutputFormat::Sarif) {
sarif::find_git_root(path)
.unwrap_or_else(|| std::fs::canonicalize(".").unwrap_or_else(|_| PathBuf::from(".")))
} else {
std::fs::canonicalize(".").unwrap_or_else(|_| PathBuf::from("."))
};
let is_machine_output = matches!(cli.format, OutputFormat::Json | OutputFormat::Sarif);
let saved_locale = if is_machine_output {
let current = rust_i18n::locale().to_string();
rust_i18n::set_locale("en");
Some(current)
} else {
None
};
let validation_start = Instant::now();
let ValidationResult {
diagnostics,
files_checked,
..
} = validate_project(path, &config)?;
if let Some(ref locale) = saved_locale {
rust_i18n::set_locale(locale);
}
let validation_duration = validation_start.elapsed();
tracing::debug!(
files_checked = files_checked,
diagnostics_count = diagnostics.len(),
"Validation complete"
);
record_telemetry_event(&diagnostics, validation_duration);
if matches!(cli.format, OutputFormat::Json) {
let json_output = json::diagnostics_to_json(&diagnostics, &base_path, files_checked);
let json_str = serde_json::to_string_pretty(&json_output)?;
println!("{}", json_str);
if json_output.summary.errors > 0 || (cli.strict && json_output.summary.warnings > 0) {
process::exit(1);
}
return Ok(());
}
if matches!(cli.format, OutputFormat::Sarif) {
let sarif = sarif::diagnostics_to_sarif(&diagnostics, &base_path);
let json = serde_json::to_string_pretty(&sarif)?;
println!("{}", json);
let has_errors = diagnostics
.iter()
.any(|d| d.level == DiagnosticLevel::Error);
let has_warnings = diagnostics
.iter()
.any(|d| d.level == DiagnosticLevel::Warning);
if has_errors || (cli.strict && has_warnings) {
process::exit(1);
}
return Ok(());
}
println!("{} {}", t!("cli.validating").cyan().bold(), path.display());
println!();
if diagnostics.is_empty() {
println!("{}", t!("cli.no_issues_found").green().bold());
return Ok(());
}
let (errors, warnings) = count_errors_warnings(&diagnostics);
let infos = diagnostics
.iter()
.filter(|d| d.level == DiagnosticLevel::Info)
.count();
let fixable = diagnostics.iter().filter(|d| d.has_fixes()).count();
for diag in &diagnostics {
let level_str = match diag.level {
DiagnosticLevel::Error => "error".red().bold(),
DiagnosticLevel::Warning => "warning".yellow().bold(),
DiagnosticLevel::Info => "info".blue().bold(),
};
let fixable_marker = if diag.has_fixes() {
format!(" {}", t!("cli.fixable")).green().to_string()
} else {
String::new()
};
println!(
"{}:{}:{} {}: {}{}",
diag.file.display().to_string().dimmed(),
diag.line,
diag.column,
level_str,
diag.message,
fixable_marker
);
if cli.verbose {
println!(" {} {}", t!("cli.rule_label").dimmed(), diag.rule.dimmed());
if let Some(ref meta) = diag.metadata {
let tool_info = match &meta.applies_to_tool {
Some(tool) => tool.as_str().into(),
None => t!("cli.generic_tool"),
};
println!(
" {} {} | {} {} | {} {}",
t!("cli.category_label").dimmed(),
meta.category,
t!("cli.severity_label").dimmed(),
meta.severity,
t!("cli.tool_label").dimmed(),
tool_info
);
}
if let Some(suggestion) = &diag.suggestion {
println!(" {} {}", t!("cli.help_label").cyan(), suggestion);
}
if let Some(assumption) = &diag.assumption {
println!(" {} {}", t!("cli.note_label").yellow(), assumption);
}
}
if cli.verbose || cli.show_fixes {
for fix in &diag.fixes {
let tier = confidence_tier_label(fix.confidence_tier());
let confidence_pct = (fix.confidence_score() * 100.0).round() as i32;
let mut qualifiers = Vec::new();
if let Some(group) = fix.group.as_deref() {
qualifiers.push(format!("group={group}"));
}
if let Some(depends_on) = fix.depends_on.as_deref() {
qualifiers.push(format!("depends_on={depends_on}"));
}
let qualifier_text = if qualifiers.is_empty() {
String::new()
} else {
format!(" [{}]", qualifiers.join(", "))
};
println!(
" {} {} ({} {}%){}",
t!("cli.fix_label").green(),
fix.description,
tier,
confidence_pct,
qualifier_text
);
}
}
println!();
}
println!("{}", "-".repeat(60).dimmed());
println!(
"{}",
t!(
"cli.found_errors_warnings",
errors = errors,
error_word = if errors == 1 {
t!("cli.error_singular")
} else {
t!("cli.error_plural")
},
warnings = warnings,
warning_word = if warnings == 1 {
t!("cli.warning_singular")
} else {
t!("cli.warning_plural")
}
)
);
if infos > 0 {
println!("{}", t!("cli.info_messages", count = infos));
}
if fixable > 0 {
println!(
"{}",
t!(
"cli.fixable_issues",
count = fixable,
word = if fixable == 1 {
t!("cli.issue_is")
} else {
t!("cli.issues_are")
}
)
);
}
let mut final_errors = errors;
let mut final_warnings = warnings;
if should_fix {
let apply_mode = resolve_fix_mode(cli);
println!();
let action_mode = if cli.dry_run {
t!("cli.preview")
} else {
t!("cli.applying")
};
let confidence_mode: String = match apply_mode {
FixApplyMode::SafeOnly => t!("cli.safe_only").to_string(),
FixApplyMode::SafeAndMedium => " (safe + medium)".to_string(),
FixApplyMode::All => " (all confidence levels)".to_string(),
};
println!(
"{}",
t!(
"cli.applying_fixes",
mode = action_mode.cyan().bold(),
safe_mode = confidence_mode
)
);
let results =
apply_fixes_with_options(&diagnostics, FixApplyOptions::new(cli.dry_run, apply_mode))?;
if results.is_empty() {
println!("{}", t!("cli.no_fixes"));
} else {
for result in &results {
println!();
println!(
" {} {}",
if cli.dry_run {
t!("cli.would_fix")
} else {
t!("cli.fixed")
}
.green(),
result.path.display()
);
for desc in &result.applied {
println!(" - {}", desc);
}
if cli.dry_run && cli.verbose {
println!();
println!(" {}:", t!("cli.diff_label").yellow());
show_diff(&result.original, &result.fixed);
}
}
println!();
let action = if cli.dry_run {
t!("cli.would_fix")
} else {
t!("cli.fixed")
};
println!(
"{}",
t!(
"cli.fix_summary",
action = action.green().bold(),
count = results.len(),
word = if results.len() == 1 {
t!("cli.file_singular")
} else {
t!("cli.file_plural")
}
)
);
}
if !cli.dry_run {
let ValidationResult {
diagnostics: post_fix_diagnostics,
files_checked: _,
..
} = validate_project(path, &config)?;
(final_errors, final_warnings) = count_errors_warnings(&post_fix_diagnostics);
}
} else if fixable > 0 {
println!();
println!(
"{} {}",
t!("cli.hint_label").cyan(),
t!(
"cli.hint_run_fix",
flag = "--fix / --fix-safe / --fix-unsafe".bold()
)
);
}
if final_errors > 0 || (cli.strict && final_warnings > 0) {
process::exit(1);
}
Ok(())
}
fn run_single_validation(
path: &Path,
strict: bool,
verbose: bool,
target: TargetArg,
config_override: Option<&PathBuf>,
) -> anyhow::Result<bool> {
let config_path = resolve_config_path(path, config_override);
let (mut config, config_warning) = LintConfig::load_or_default(config_path.as_ref());
if let Some(warning) = config_warning {
eprintln!("{} {}", t!("cli.warning_label").yellow().bold(), warning);
eprintln!();
}
config.set_target(target.into());
let ValidationResult {
diagnostics,
files_checked: _,
..
} = validate_project(path, &config)?;
println!("{} {}", t!("cli.validating").cyan().bold(), path.display());
println!();
if diagnostics.is_empty() {
println!("{}", t!("cli.no_issues_found").green().bold());
return Ok(false);
}
let errors = diagnostics
.iter()
.filter(|d| d.level == DiagnosticLevel::Error)
.count();
let warnings = diagnostics
.iter()
.filter(|d| d.level == DiagnosticLevel::Warning)
.count();
for diag in &diagnostics {
let level_str = match diag.level {
DiagnosticLevel::Error => "error".red().bold(),
DiagnosticLevel::Warning => "warning".yellow().bold(),
DiagnosticLevel::Info => "info".blue().bold(),
};
println!(
"{}:{}:{} {}: {}",
diag.file.display().to_string().dimmed(),
diag.line,
diag.column,
level_str,
diag.message,
);
if verbose {
println!(" {} {}", t!("cli.rule_label").dimmed(), diag.rule.dimmed());
if let Some(suggestion) = &diag.suggestion {
println!(" {} {}", t!("cli.help_label").cyan(), suggestion);
}
}
println!();
}
println!("{}", "-".repeat(60).dimmed());
println!(
"{}",
t!(
"cli.found_errors_warnings",
errors = errors,
error_word = if errors == 1 {
t!("cli.error_singular")
} else {
t!("cli.error_plural")
},
warnings = warnings,
warning_word = if warnings == 1 {
t!("cli.warning_singular")
} else {
t!("cli.warning_plural")
}
)
);
Ok(errors > 0 || (strict && warnings > 0))
}
fn resolve_config_path(path: &Path, config_override: Option<&PathBuf>) -> Option<PathBuf> {
if let Some(config) = config_override {
return Some(config.clone());
}
let mut candidates = Vec::new();
if path.is_dir() {
candidates.push(path.to_path_buf());
} else if let Some(parent) = path.parent() {
candidates.push(parent.to_path_buf());
}
if let Ok(cwd) = env::current_dir() {
candidates.push(cwd);
}
for dir in candidates {
let candidate = dir.join(".agnix.toml");
if candidate.exists() {
return Some(candidate);
}
}
None
}
fn resolve_fix_mode(cli: &Cli) -> FixApplyMode {
if cli.fix_safe {
FixApplyMode::SafeOnly
} else if cli.fix_unsafe {
FixApplyMode::All
} else {
FixApplyMode::SafeAndMedium
}
}
#[cfg(test)]
mod resolve_fix_mode_tests {
use super::*;
#[test]
fn fix_safe_selects_safe_only_mode() {
let cli = Cli::parse_from(["agnix", "--fix-safe"]);
assert_eq!(resolve_fix_mode(&cli), FixApplyMode::SafeOnly);
}
#[test]
fn fix_unsafe_selects_all_mode() {
let cli = Cli::parse_from(["agnix", "--fix-unsafe"]);
assert_eq!(resolve_fix_mode(&cli), FixApplyMode::All);
}
#[test]
fn fix_selects_safe_and_medium_mode() {
let cli = Cli::parse_from(["agnix", "--fix"]);
assert_eq!(resolve_fix_mode(&cli), FixApplyMode::SafeAndMedium);
}
#[test]
fn dry_run_selects_safe_and_medium_mode() {
let cli = Cli::parse_from(["agnix", "--dry-run"]);
assert_eq!(resolve_fix_mode(&cli), FixApplyMode::SafeAndMedium);
}
#[test]
fn dry_run_with_fix_safe_selects_safe_only_mode() {
let cli = Cli::parse_from(["agnix", "--dry-run", "--fix-safe"]);
assert_eq!(resolve_fix_mode(&cli), FixApplyMode::SafeOnly);
}
#[test]
fn dry_run_with_fix_unsafe_selects_all_mode() {
let cli = Cli::parse_from(["agnix", "--dry-run", "--fix-unsafe"]);
assert_eq!(resolve_fix_mode(&cli), FixApplyMode::All);
}
}
fn confidence_tier_label(tier: FixConfidenceTier) -> &'static str {
match tier {
FixConfidenceTier::High => "HIGH",
FixConfidenceTier::Medium => "MEDIUM",
FixConfidenceTier::Low => "LOW",
}
}
fn show_diff(original: &str, fixed: &str) {
let diff = TextDiff::from_lines(original, fixed);
for change in diff.iter_all_changes() {
match change.tag() {
ChangeTag::Delete => print!(" {} {}", "-".red(), change.to_string().red()),
ChangeTag::Insert => print!(" {} {}", "+".green(), change.to_string().green()),
ChangeTag::Equal => {}
}
}
}
fn init_command(output: &PathBuf) -> anyhow::Result<()> {
let default_config = LintConfig::default();
let toml_content = toml::to_string_pretty(&default_config)?;
std::fs::write(output, toml_content)?;
println!("{} {}", t!("cli.created").green().bold(), output.display());
Ok(())
}
fn schema_command(output: Option<&PathBuf>) -> anyhow::Result<()> {
let schema = generate_schema();
let json = serde_json::to_string_pretty(&schema)?;
match output {
Some(path) => {
std::fs::write(path, &json)?;
println!(
"{} {}",
t!("cli.schema_written").green().bold(),
path.display()
);
}
None => {
println!("{}", json);
}
}
Ok(())
}
fn eval_command(
path: &Path,
format: EvalOutputFormat,
filter: Option<&str>,
verbose: bool,
) -> anyhow::Result<()> {
let config = LintConfig::default();
println!("{} {}", t!("cli.evaluating").cyan().bold(), path.display());
if let Some(f) = filter {
println!(" {} {}", t!("cli.filter_label").dimmed(), f);
}
println!();
let (results, summary) = evaluate_manifest_file(path, &config, filter)?;
if verbose {
println!("{}", t!("cli.per_case_results").cyan().bold());
println!("{}", "=".repeat(60).dimmed());
for result in &results {
let status = if result.passed() {
t!("cli.pass").green().bold()
} else {
t!("cli.fail").red().bold()
};
println!("[{}] {}", status, result.case.file.display());
if let Some(desc) = &result.case.description {
println!(" {}", desc.dimmed());
}
if !result.passed() {
if !result.false_positives.is_empty() {
println!(
" {} {:?}",
t!("cli.unexpected_label").yellow(),
result.false_positives
);
}
if !result.false_negatives.is_empty() {
println!(
" {} {:?}",
t!("cli.missing_label").red(),
result.false_negatives
);
}
}
println!();
}
println!("{}", "=".repeat(60).dimmed());
println!();
}
let eval_format: EvalFormat = format.into();
match eval_format {
EvalFormat::Json => {
let json = summary.to_json()?;
println!("{}", json);
}
EvalFormat::Csv => {
let csv = summary.to_csv();
println!("{}", csv);
}
EvalFormat::Markdown => {
let md = summary.to_markdown();
println!("{}", md);
}
}
println!();
if summary.cases_failed == 0 {
println!(
"{} {}",
t!("cli.success").green().bold(),
t!("cli.all_cases_passed", count = summary.cases_run)
);
} else {
println!(
"{} {}",
t!("cli.failed").red().bold(),
t!(
"cli.cases_failed",
failed = summary.cases_failed,
total = summary.cases_run
)
);
process::exit(1);
}
Ok(())
}
fn record_telemetry_event(diagnostics: &[agnix_core::Diagnostic], duration: std::time::Duration) {
use agnix_core::DiagnosticLevel;
let mut error_count = 0u32;
let mut warning_count = 0u32;
let mut info_count = 0u32;
let mut rule_trigger_counts: HashMap<String, u32> = HashMap::new();
for diag in diagnostics {
match diag.level {
DiagnosticLevel::Error => error_count += 1,
DiagnosticLevel::Warning => warning_count += 1,
DiagnosticLevel::Info => info_count += 1,
}
if telemetry::is_valid_rule_id(&diag.rule) {
*rule_trigger_counts.entry(diag.rule.clone()).or_insert(0) += 1;
}
}
let file_type_counts: HashMap<String, u32> = HashMap::new();
telemetry::record_validation(
file_type_counts,
rule_trigger_counts,
error_count,
warning_count,
info_count,
duration.as_millis() as u64,
);
}
fn telemetry_command(action: TelemetryAction) -> anyhow::Result<()> {
use telemetry::TelemetryConfig;
match action {
TelemetryAction::Status => {
let config = TelemetryConfig::load().unwrap_or_default();
let effective = config.is_enabled();
println!("{}", t!("cli.telemetry_status").cyan().bold());
println!();
println!(
" {} {}",
t!("cli.telemetry_configured").dimmed(),
if config.enabled {
t!("cli.telemetry_enabled")
} else {
t!("cli.telemetry_disabled")
}
);
println!(
" {} {}",
t!("cli.telemetry_effective").dimmed(),
if effective {
t!("cli.telemetry_enabled")
} else {
t!("cli.telemetry_disabled")
}
);
if config.enabled && !effective {
println!();
println!(
" {} {}",
t!("cli.note_label").yellow(),
t!("cli.telemetry_env_note")
);
}
if let Some(id) = &config.installation_id {
let short_id = if id.len() > 8 { &id[..8] } else { id };
println!(
" {} {}...",
t!("cli.telemetry_installation_id").dimmed(),
short_id
);
}
if let Some(ts) = &config.consent_timestamp {
println!(" {} {}", t!("cli.telemetry_consent_given").dimmed(), ts);
}
println!();
println!("{}", t!("cli.telemetry_privacy").cyan().bold());
println!("{}", t!("cli.telemetry_privacy_1"));
println!("{}", t!("cli.telemetry_privacy_2"));
println!("{}", t!("cli.telemetry_privacy_3"));
println!("{}", t!("cli.telemetry_privacy_4"));
println!("{}", t!("cli.telemetry_privacy_5"));
if let Ok(path) = TelemetryConfig::config_path() {
println!();
println!(
" {} {}",
t!("cli.telemetry_config_file").dimmed(),
path.display()
);
}
}
TelemetryAction::Enable => {
let mut config = TelemetryConfig::load().unwrap_or_default();
if config.enabled {
println!(
"{} {}",
t!("cli.note_label").cyan(),
t!("cli.telemetry_already_enabled")
);
} else {
config.enable()?;
println!("{} {}", "OK".green().bold(), t!("cli.telemetry_ok_enabled"));
println!();
println!("{}", t!("cli.telemetry_thanks"));
println!();
println!("{}", t!("cli.telemetry_collect").cyan());
println!("{}", t!("cli.telemetry_collect_1"));
println!("{}", t!("cli.telemetry_collect_2"));
println!("{}", t!("cli.telemetry_collect_3"));
println!("{}", t!("cli.telemetry_collect_4"));
println!();
println!("{}", t!("cli.telemetry_never_collect").cyan());
println!("{}", t!("cli.telemetry_never_1"));
println!("{}", t!("cli.telemetry_never_2"));
println!("{}", t!("cli.telemetry_never_3"));
println!();
println!(
"{}",
t!(
"cli.telemetry_disable_hint",
cmd = "agnix telemetry disable".bold()
)
);
}
}
TelemetryAction::Disable => {
let mut config = TelemetryConfig::load().unwrap_or_default();
if !config.enabled {
println!(
"{} {}",
t!("cli.note_label").cyan(),
t!("cli.telemetry_already_disabled")
);
} else {
config.disable()?;
println!(
"{} {}",
"OK".green().bold(),
t!("cli.telemetry_ok_disabled")
);
}
}
}
Ok(())
}