Skip to main content

ironflow_cli/commands/
agent_role.rs

1//! Agent role subcommands: list, show, set-cap, clear-cap.
2
3use std::slice::from_ref;
4
5use anyhow::{Result, bail};
6use clap::{Args, Subcommand};
7use comfy_table::{ContentArrangement, Table};
8use ironflow_sdk::IronflowClient;
9use ironflow_sdk::types::{AgentRoleResponse, UpdateAgentRoleCostCapRequest};
10
11use crate::output;
12
13/// Arguments for the `agent-role` command group.
14#[derive(Debug, Args)]
15pub struct AgentRoleArgs {
16    /// Agent role subcommand.
17    #[command(subcommand)]
18    pub command: AgentRoleCommands,
19}
20
21/// Available agent role subcommands.
22#[derive(Debug, Subcommand)]
23pub enum AgentRoleCommands {
24    /// List agent roles with the cost spent, reserved and left this period.
25    List {
26        /// Page number (1-based).
27        #[arg(long)]
28        page: Option<u32>,
29        /// Items per page (max 100).
30        #[arg(long)]
31        per_page: Option<u32>,
32    },
33    /// Show the cost and cap of one agent role.
34    Show {
35        /// Role name, as declared by `AgentRole::name`.
36        name: String,
37    },
38    /// Set the operator cap of a role (admin only). It can only lower the
39    /// cap declared in code.
40    SetCap {
41        /// Role name.
42        name: String,
43        /// Cap in USD over the role's period, at least 0 (0 disables the role).
44        #[arg(long)]
45        usd: f64,
46    },
47    /// Clear the operator cap of a role (admin only): the code cap applies.
48    ClearCap {
49        /// Role name.
50        name: String,
51    },
52}
53
54/// `$x.xxxx`, or `-` when there is no value.
55fn usd(value: Option<f64>) -> String {
56    value.map_or_else(|| "-".to_string(), |v| format!("${v:.4}"))
57}
58
59fn roles_table(items: &[AgentRoleResponse]) -> Table {
60    let mut table = Table::new();
61    table.set_content_arrangement(ContentArrangement::Dynamic);
62    table.set_header(vec![
63        "NAME",
64        "PERIOD",
65        "SPENT",
66        "RESERVED",
67        "CAP",
68        "REMAINING",
69        "RESETS AT",
70    ]);
71    for role in items {
72        table.add_row(vec![
73            role.name.clone(),
74            role.period.clone(),
75            usd(Some(role.spent_usd)),
76            usd(Some(role.reserved_usd)),
77            usd(role.cap_usd),
78            usd(role.remaining_usd),
79            role.resets_at.to_string(),
80        ]);
81    }
82    table
83}
84
85/// Build the cap update request, refusing a negative or non-finite cap.
86fn cap_request(usd: Option<f64>) -> Result<UpdateAgentRoleCostCapRequest> {
87    if let Some(cap) = usd
88        && !(cap.is_finite() && cap >= 0.0)
89    {
90        bail!("--usd must be a number at least 0, got {cap}");
91    }
92    Ok(UpdateAgentRoleCostCapRequest {
93        operator_cap_usd: usd,
94    })
95}
96
97/// Execute an agent role subcommand.
98///
99/// # Errors
100///
101/// Returns an error on API failure or an invalid `--usd`.
102pub async fn execute(client: &IronflowClient, args: &AgentRoleArgs, json_mode: bool) -> Result<()> {
103    match &args.command {
104        AgentRoleCommands::List { page, per_page } => {
105            let response = client.list_agent_roles(*page, *per_page).await?;
106            output::print_output(json_mode, &response, || roles_table(&response.data))
107        }
108        AgentRoleCommands::Show { name } => {
109            let response = client.get_agent_role(name).await?;
110            output::print_output(json_mode, &response, || {
111                roles_table(from_ref(&response.data))
112            })
113        }
114        AgentRoleCommands::SetCap { name, usd } => {
115            let request = cap_request(Some(*usd))?;
116            let response = client.set_agent_role_cost_cap(name, &request).await?;
117            output::print_output(json_mode, &response, || {
118                roles_table(from_ref(&response.data))
119            })
120        }
121        AgentRoleCommands::ClearCap { name } => {
122            let request = cap_request(None)?;
123            let response = client.set_agent_role_cost_cap(name, &request).await?;
124            output::print_output(json_mode, &response, || {
125                roles_table(from_ref(&response.data))
126            })
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn usd_formats_a_value_or_a_dash() {
137        assert_eq!(usd(Some(1.5)), "$1.5000");
138        assert_eq!(usd(None), "-");
139    }
140
141    #[test]
142    fn cap_request_refuses_a_negative_or_nan_cap() {
143        assert!(cap_request(Some(-1.0)).is_err());
144        assert!(cap_request(Some(f64::NAN)).is_err());
145        assert_eq!(cap_request(Some(2.5)).unwrap().operator_cap_usd, Some(2.5));
146        assert_eq!(cap_request(None).unwrap().operator_cap_usd, None);
147    }
148}