nils-macos-agent 0.6.2

CLI crate for nils-macos-agent in the nils-cli workspace.
Documentation
use std::time::Instant;

use crate::backend::process::ProcessRunner;
use crate::backend::{AutoAxBackend, AxBackendAdapter};
use crate::cli::{AxActionPerformArgs, OutputFormat};
use crate::commands::ax_common::{build_selector_from_args, build_target_from_args};
use crate::commands::{emit_json_success, reject_tsv_for_list_only};
use crate::error::CliError;
use crate::model::{AxActionPerformCommandResult, AxActionPerformRequest, AxActionPerformResult};
use crate::retry::run_with_retry;
use crate::run::{
    ActionPolicy, action_policy_result, build_action_meta_with_attempts, next_action_id,
};

pub fn run_perform(
    format: OutputFormat,
    args: &AxActionPerformArgs,
    policy: ActionPolicy,
    runner: &dyn ProcessRunner,
) -> Result<(), CliError> {
    let request = AxActionPerformRequest {
        target: build_target_from_args(&args.target)?,
        selector: build_selector_from_args(&args.selector)?,
        name: args.name.clone(),
    };

    let action_id = next_action_id("ax.action.perform");
    let started = Instant::now();
    let mut attempts_used = 0u8;
    let mut detail = AxActionPerformResult {
        node_id: request.selector.node_id.clone(),
        matched_count: 0,
        name: request.name.clone(),
        performed: false,
    };

    if !policy.dry_run {
        let backend = AutoAxBackend::default();
        let retry = policy.retry_policy();
        let (backend_result, attempts) = run_with_retry(retry, || {
            backend.action_perform(runner, &request, policy.timeout_ms)
        })?;
        attempts_used = attempts;
        detail = backend_result;
    }

    let result = AxActionPerformCommandResult {
        detail,
        policy: action_policy_result(policy),
        meta: build_action_meta_with_attempts(action_id, started, policy, attempts_used),
    };

    match format {
        OutputFormat::Json => {
            emit_json_success("ax.action.perform", result)?;
        }
        OutputFormat::Text => {
            println!(
                "ax.action.perform\tnode_id={}\tname={}\tmatched_count={}\tperformed={}\taction_id={}\telapsed_ms={}",
                result.detail.node_id.clone().unwrap_or_default(),
                result.detail.name,
                result.detail.matched_count,
                result.detail.performed,
                result.meta.action_id,
                result.meta.elapsed_ms,
            );
        }
        OutputFormat::Tsv => {
            return reject_tsv_for_list_only();
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use nils_test_support::{EnvGuard, GlobalStateLock};

    use super::run_perform;
    use crate::backend::process::RealProcessRunner;
    use crate::cli::{AxActionPerformArgs, OutputFormat};
    use crate::run::ActionPolicy;

    fn policy(dry_run: bool) -> ActionPolicy {
        ActionPolicy {
            dry_run,
            retries: 0,
            retry_delay_ms: 150,
            timeout_ms: 1000,
        }
    }

    fn sample_args() -> AxActionPerformArgs {
        AxActionPerformArgs {
            selector: crate::cli::AxSelectorArgs {
                node_id: Some("1.1".to_string()),
                ..crate::cli::AxSelectorArgs::default()
            },
            target: crate::cli::AxTargetArgs::default(),
            name: "AXPress".to_string(),
        }
    }

    #[test]
    fn run_perform_dry_run_supports_text_and_json() {
        let lock = GlobalStateLock::new();
        let _mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
        let runner = RealProcessRunner;

        run_perform(OutputFormat::Text, &sample_args(), policy(true), &runner)
            .expect("text dry-run should succeed");
        run_perform(OutputFormat::Json, &sample_args(), policy(true), &runner)
            .expect("json dry-run should succeed");
    }

    #[test]
    fn run_perform_rejects_tsv() {
        let lock = GlobalStateLock::new();
        let _mode = EnvGuard::set(&lock, "AGENTS_MACOS_AGENT_TEST_MODE", "1");
        let runner = RealProcessRunner;

        let err = run_perform(OutputFormat::Tsv, &sample_args(), policy(true), &runner)
            .expect_err("tsv should be rejected");
        assert!(err.to_string().contains("windows list"));
    }
}