Skip to main content

ironflow_cli/commands/
account.rs

1//! `ironflow accounts` -- manage Provider Accounts (admin only).
2//!
3//! The token is read from stdin only (`--token-stdin`), which keeps it out of
4//! the shell history and out of `ps` output. No command prints it back.
5
6use std::slice;
7
8use anyhow::{Context, Result};
9use chrono::{DateTime, Utc};
10use clap::{Args, Subcommand, value_parser};
11use ironflow_sdk::IronflowClient;
12use ironflow_sdk::types::{CreateProviderAccountRequest, UpdateProviderAccountRequest};
13
14use crate::confirm::{confirm, resolve_secret_value};
15use crate::output;
16
17/// Arguments of `ironflow accounts`.
18#[derive(Debug, Args)]
19pub struct AccountArgs {
20    /// Account subcommand.
21    #[command(subcommand)]
22    pub command: AccountCommands,
23}
24
25/// `ironflow accounts` subcommands.
26#[derive(Debug, Subcommand)]
27pub enum AccountCommands {
28    /// Add an account. The token is checked against the provider first.
29    Add {
30        /// Unique name (lowercase slug, e.g. `perso-max`).
31        name: String,
32        /// Account kind.
33        #[arg(long, default_value = "claude_subscription")]
34        kind: String,
35        /// Human-readable name.
36        #[arg(long)]
37        display_name: Option<String>,
38        /// Read the token (e.g. from `claude setup-token`) from stdin.
39        #[arg(long, required = true)]
40        token_stdin: bool,
41        /// Tag (repeatable).
42        #[arg(long = "tag")]
43        tags: Vec<String>,
44        /// Priority, lower is preferred.
45        #[arg(long)]
46        priority: Option<i32>,
47        /// Maximum concurrent steps.
48        #[arg(long, value_parser = value_parser!(i32).range(1..))]
49        max_concurrency: Option<i32>,
50        /// Utilization from which the account is shown as near its limit, in (0, 1].
51        #[arg(long)]
52        alert_threshold: Option<f64>,
53        /// Subscription plan (`pro`, `max`).
54        #[arg(long)]
55        plan: Option<String>,
56        /// When the token expires (RFC 3339).
57        #[arg(long)]
58        expires_at: Option<DateTime<Utc>>,
59        /// Add the account disabled.
60        #[arg(long)]
61        disabled: bool,
62    },
63    /// List accounts with their current usage.
64    List {
65        /// Only accounts of this kind.
66        #[arg(long)]
67        kind: Option<String>,
68    },
69    /// Show one account and its windows.
70    Show {
71        /// Account name or UUID.
72        account: String,
73    },
74    /// Show the current windows of an account.
75    Usage {
76        /// Account name or UUID.
77        account: String,
78    },
79    /// Update an account. Omitted options are left unchanged.
80    Update {
81        /// Account name or UUID.
82        account: String,
83        /// New display name.
84        #[arg(long)]
85        display_name: Option<String>,
86        /// Enable the account.
87        #[arg(long, conflicts_with = "disable")]
88        enable: bool,
89        /// Disable the account.
90        #[arg(long)]
91        disable: bool,
92        /// Tag (repeatable, replaces the list).
93        #[arg(long = "tag")]
94        tags: Vec<String>,
95        /// New priority.
96        #[arg(long)]
97        priority: Option<i32>,
98        /// New maximum concurrent steps.
99        #[arg(
100            long,
101            conflicts_with = "clear_max_concurrency",
102            value_parser = value_parser!(i32).range(1..)
103        )]
104        max_concurrency: Option<i32>,
105        /// Remove the concurrency limit.
106        #[arg(long)]
107        clear_max_concurrency: bool,
108        /// New alert threshold, in (0, 1].
109        #[arg(long)]
110        alert_threshold: Option<f64>,
111        /// New plan.
112        #[arg(long)]
113        plan: Option<String>,
114        /// Replace the token, read from stdin.
115        #[arg(long)]
116        token_stdin: bool,
117    },
118    /// Delete an account and its token.
119    Remove {
120        /// Account name or UUID.
121        account: String,
122        /// Skip the interactive confirmation.
123        #[arg(long)]
124        yes: bool,
125    },
126    /// Check the stored token against the provider.
127    Test {
128        /// Account name or UUID.
129        account: String,
130    },
131}
132
133/// Execute an `ironflow accounts` subcommand.
134///
135/// # Errors
136///
137/// Returns an error when the API call fails, the token cannot be read, or
138/// the user declines a confirmation.
139pub async fn execute(client: &IronflowClient, args: &AccountArgs, json_mode: bool) -> Result<()> {
140    match &args.command {
141        AccountCommands::Add {
142            name,
143            kind,
144            display_name,
145            token_stdin: _,
146            tags,
147            priority,
148            max_concurrency,
149            alert_threshold,
150            plan,
151            expires_at,
152            disabled,
153        } => {
154            let token = resolve_secret_value(None, "token")?;
155            let request: CreateProviderAccountRequest = CreateProviderAccountRequest::builder()
156                .name(name.clone())
157                .kind(kind.clone())
158                .token(token)
159                .display_name(display_name.clone())
160                .enabled(Some(!disabled))
161                .tags((!tags.is_empty()).then(|| tags.clone()))
162                .priority(*priority)
163                .max_concurrency(*max_concurrency)
164                .alert_threshold(*alert_threshold)
165                .plan(plan.clone())
166                .expires_at(*expires_at)
167                .try_into()
168                .context("failed to build CreateProviderAccountRequest")?;
169            let response = client.create_provider_account(&request).await?;
170            output::print_output(json_mode, &response, || {
171                output::provider_accounts_table(slice::from_ref(&response.data))
172            })?;
173        }
174        AccountCommands::List { kind } => {
175            let mut response = client.list_provider_accounts().await?;
176            if let Some(kind) = kind {
177                response.data.retain(|a| &a.kind == kind);
178            }
179            output::print_output(json_mode, &response, || {
180                output::provider_accounts_table(&response.data)
181            })?;
182        }
183        AccountCommands::Show { account } => {
184            let response = client.get_provider_account(account).await?;
185            output::print_output(json_mode, &response, || {
186                output::provider_accounts_table(slice::from_ref(&response.data))
187            })?;
188            if !json_mode {
189                println!(
190                    "{}",
191                    output::provider_account_windows_table(&response.data.windows)
192                );
193            }
194        }
195        AccountCommands::Usage { account } => {
196            let response = client.provider_account_usage(account).await?;
197            output::print_output(json_mode, &response, || {
198                output::provider_account_windows_table(&response.data.windows)
199            })?;
200        }
201        AccountCommands::Update {
202            account,
203            display_name,
204            enable,
205            disable,
206            tags,
207            priority,
208            max_concurrency,
209            clear_max_concurrency,
210            alert_threshold,
211            plan,
212            token_stdin,
213        } => {
214            let token = if *token_stdin {
215                Some(resolve_secret_value(None, "token")?)
216            } else {
217                None
218            };
219            let enabled = match (*enable, *disable) {
220                (true, _) => Some(true),
221                (_, true) => Some(false),
222                _ => None,
223            };
224            let request: UpdateProviderAccountRequest = UpdateProviderAccountRequest::builder()
225                .display_name(display_name.clone())
226                .enabled(enabled)
227                .tags((!tags.is_empty()).then(|| tags.clone()))
228                .priority(*priority)
229                .max_concurrency(*max_concurrency)
230                .alert_threshold(*alert_threshold)
231                .plan(plan.clone())
232                .token(token)
233                .try_into()
234                .context("failed to build UpdateProviderAccountRequest")?;
235            let mut response = client.update_provider_account(account, &request).await?;
236            if *clear_max_concurrency {
237                response = client
238                    .clear_provider_account_max_concurrency(account)
239                    .await?;
240            }
241            output::print_output(json_mode, &response, || {
242                output::provider_accounts_table(slice::from_ref(&response.data))
243            })?;
244        }
245        AccountCommands::Remove { account, yes } => {
246            confirm(&format!("Delete provider account '{account}'?"), *yes)?;
247            client.delete_provider_account(account).await?;
248            output::report_deletion(json_mode, "provider account", account.clone())?;
249        }
250        AccountCommands::Test { account } => {
251            let response = client.test_provider_account(account).await?;
252            output::print_output(json_mode, &response, || {
253                output::provider_account_windows_table(&response.data.windows)
254            })?;
255            if !json_mode {
256                println!("result: {}", response.data.result);
257            }
258        }
259    }
260    Ok(())
261}