Skip to main content

codex_ops/
cli.rs

1use crate::limits::{LimitCommand, LimitCommandOptions};
2use crate::stats::StatCommandOptions;
3use clap::error::ErrorKind;
4use clap::{Args, CommandFactory, Parser, Subcommand};
5use std::env;
6use std::path::PathBuf;
7
8const ROOT_USAGE: &str = "codex-ops <command> [options]";
9const AUTH_USAGE: &str = "codex-ops auth <command> [options]";
10const AUTH_STATUS_USAGE: &str = "codex-ops auth status [options]";
11const AUTH_SAVE_USAGE: &str = "codex-ops auth save [options]";
12const AUTH_LIST_USAGE: &str = "codex-ops auth list [options]";
13const AUTH_SELECT_USAGE: &str = "codex-ops auth select [options]";
14const AUTH_REMOVE_USAGE: &str = "codex-ops auth remove [options]";
15const DOCTOR_USAGE: &str = "codex-ops doctor [options]";
16const LIMIT_USAGE: &str = "codex-ops limit <command> [options]";
17
18#[derive(Debug, Clone, Eq, PartialEq)]
19pub enum ParsedCli {
20    Command(Box<CliCommand>),
21    Help(String),
22    Version,
23}
24
25#[derive(Debug, Clone, Eq, PartialEq)]
26pub enum CliCommand {
27    Auth(AuthCliCommand),
28    Doctor(DoctorCliCommand),
29    Stat(StatCliCommand),
30    Limit(LimitCliCommand),
31}
32
33#[derive(Debug, Clone, Eq, PartialEq)]
34pub enum AuthCliCommand {
35    Status(AuthStatusCliOptions),
36    Save(AuthProfileCliOptions),
37    List(AuthProfileCliOptions),
38    Select(AuthSelectCliOptions),
39    Remove(AuthRemoveCliOptions),
40}
41
42#[derive(Debug, Clone, Eq, PartialEq)]
43pub enum DoctorCliCommand {
44    Run(DoctorCliOptions),
45}
46
47#[derive(Debug, Clone, Eq, PartialEq)]
48pub struct StatCliCommand {
49    pub view: Option<String>,
50    pub session: Option<String>,
51    pub options: StatCommandOptions,
52}
53
54#[derive(Debug, Clone, Eq, PartialEq)]
55pub struct LimitCliCommand {
56    pub command: LimitCommand,
57    pub options: LimitCommandOptions,
58}
59
60#[derive(Debug, Clone, Default, Eq, PartialEq)]
61pub struct AuthCliPaths {
62    pub auth_file: Option<PathBuf>,
63    pub codex_home: Option<PathBuf>,
64    pub store_dir: Option<PathBuf>,
65    pub account_history_file: Option<PathBuf>,
66}
67
68#[derive(Debug, Clone, Eq, PartialEq)]
69pub struct AuthStatusCliOptions {
70    pub paths: AuthCliPaths,
71    pub json: bool,
72    pub include_token_claims: bool,
73}
74
75#[derive(Debug, Clone, Eq, PartialEq)]
76pub struct AuthProfileCliOptions {
77    pub paths: AuthCliPaths,
78}
79
80#[derive(Debug, Clone, Eq, PartialEq)]
81pub struct AuthSelectCliOptions {
82    pub paths: AuthCliPaths,
83    pub account_id: Option<String>,
84}
85
86#[derive(Debug, Clone, Eq, PartialEq)]
87pub struct AuthRemoveCliOptions {
88    pub paths: AuthCliPaths,
89    pub account_id: Option<String>,
90    pub yes: bool,
91}
92
93#[derive(Debug, Clone, Default, Eq, PartialEq)]
94pub struct DoctorCliPaths {
95    pub auth_file: Option<PathBuf>,
96    pub codex_home: Option<PathBuf>,
97    pub sessions_dir: Option<PathBuf>,
98}
99
100#[derive(Debug, Clone, Eq, PartialEq)]
101pub struct DoctorCliOptions {
102    pub paths: DoctorCliPaths,
103    pub json: bool,
104}
105
106#[derive(Debug, Clone, Eq, PartialEq)]
107pub struct CliParseError {
108    pub code: i32,
109    pub message: String,
110}
111
112impl CliParseError {
113    fn new(code: i32, message: impl Into<String>) -> Self {
114        Self {
115            code,
116            message: message.into(),
117        }
118    }
119}
120
121#[derive(Debug, Parser)]
122#[command(
123    name = "codex-ops",
124    disable_help_subcommand = true,
125    disable_version_flag = true,
126    override_usage = ROOT_USAGE,
127    color = clap::ColorChoice::Never
128)]
129struct CliArgs {
130    #[arg(short = 'V', long, help = "Print version")]
131    version: bool,
132
133    #[command(subcommand)]
134    command: Option<RootCommand>,
135}
136
137#[derive(Debug, Subcommand)]
138enum RootCommand {
139    #[command(
140        about = "Show and manage Codex authentication information",
141        override_usage = AUTH_USAGE
142    )]
143    Auth(AuthArgs),
144    #[command(
145        about = "Check local Codex Ops configuration and data",
146        override_usage = DOCTOR_USAGE
147    )]
148    Doctor(DoctorArgs),
149    #[command(
150        about = "Show Codex session token usage statistics",
151        override_usage = "codex-ops stat [view] [session] [options]"
152    )]
153    Stat(StatArgs),
154    #[command(
155        about = "Show Codex server rate-limit telemetry",
156        override_usage = LIMIT_USAGE
157    )]
158    Limit(LimitArgs),
159}
160
161#[derive(Debug, Args)]
162struct AuthArgs {
163    #[command(subcommand)]
164    command: Option<AuthCommand>,
165}
166
167#[derive(Debug, Subcommand)]
168enum AuthCommand {
169    #[command(
170        about = "Decode auth.json and show key claims",
171        override_usage = AUTH_STATUS_USAGE
172    )]
173    Status(AuthStatusArgs),
174    #[command(
175        about = "Persist the current auth.json by account id",
176        override_usage = AUTH_SAVE_USAGE
177    )]
178    Save(AuthProfileArgs),
179    #[command(
180        about = "List current and persisted auth profiles",
181        override_usage = AUTH_LIST_USAGE
182    )]
183    List(AuthProfileArgs),
184    #[command(
185        about = "Activate a persisted auth profile",
186        override_usage = AUTH_SELECT_USAGE
187    )]
188    Select(AuthSelectArgs),
189    #[command(
190        about = "Remove persisted auth profiles",
191        override_usage = AUTH_REMOVE_USAGE
192    )]
193    Remove(AuthRemoveArgs),
194}
195
196#[derive(Debug, Args)]
197struct AuthStatusArgs {
198    #[arg(long, value_name = "path", help = "Path to auth.json")]
199    auth_file: Option<PathBuf>,
200    #[arg(long, value_name = "path", help = "Codex home directory")]
201    codex_home: Option<PathBuf>,
202    #[arg(short = 'j', long, help = "Print JSON")]
203    json: bool,
204    #[arg(long, help = "Include decoded JWT header and claims in JSON output")]
205    include_token_claims: bool,
206}
207
208#[derive(Debug, Args)]
209struct AuthProfileArgs {
210    #[arg(long, value_name = "path", help = "Path to auth.json")]
211    auth_file: Option<PathBuf>,
212    #[arg(long, value_name = "path", help = "Codex home directory")]
213    codex_home: Option<PathBuf>,
214    #[arg(long, value_name = "path", help = "Auth profile store directory")]
215    store_dir: Option<PathBuf>,
216}
217
218#[derive(Debug, Args)]
219struct AuthSelectArgs {
220    #[arg(long, value_name = "path", help = "Path to auth.json")]
221    auth_file: Option<PathBuf>,
222    #[arg(long, value_name = "path", help = "Codex home directory")]
223    codex_home: Option<PathBuf>,
224    #[arg(long, value_name = "path", help = "Auth profile store directory")]
225    store_dir: Option<PathBuf>,
226    #[arg(long, value_name = "path", help = "Auth account history file")]
227    account_history_file: Option<PathBuf>,
228    #[arg(
229        short = 'A',
230        long,
231        value_name = "id",
232        help = "Activate a specific persisted account id"
233    )]
234    account_id: Option<String>,
235}
236
237#[derive(Debug, Args)]
238struct AuthRemoveArgs {
239    #[arg(long, value_name = "path", help = "Path to auth.json")]
240    auth_file: Option<PathBuf>,
241    #[arg(long, value_name = "path", help = "Codex home directory")]
242    codex_home: Option<PathBuf>,
243    #[arg(long, value_name = "path", help = "Auth profile store directory")]
244    store_dir: Option<PathBuf>,
245    #[arg(
246        short = 'A',
247        long,
248        value_name = "id",
249        help = "Remove a specific persisted account id"
250    )]
251    account_id: Option<String>,
252    #[arg(
253        short = 'y',
254        long,
255        help = "Skip confirmation when --account-id is supplied"
256    )]
257    yes: bool,
258}
259
260#[derive(Debug, Args)]
261struct DoctorArgs {
262    #[arg(long, value_name = "path", help = "Path to auth.json")]
263    auth_file: Option<PathBuf>,
264    #[arg(long, value_name = "path", help = "Codex home directory")]
265    codex_home: Option<PathBuf>,
266    #[arg(long, value_name = "path", help = "Codex sessions directory")]
267    sessions_dir: Option<PathBuf>,
268    #[arg(short = 'j', long, help = "Print JSON")]
269    json: bool,
270}
271
272#[derive(Debug, Args)]
273struct StatArgs {
274    #[arg(value_name = "view")]
275    view: Option<String>,
276    #[arg(value_name = "session")]
277    session: Option<String>,
278    #[command(flatten)]
279    options: StatOptionArgs,
280}
281
282#[derive(Debug, Args)]
283struct StatOptionArgs {
284    #[arg(
285        short = 'g',
286        long,
287        value_name = "group",
288        help = "hour, day, week, month, model, cwd, account"
289    )]
290    group_by: Option<String>,
291    #[arg(
292        long,
293        value_name = "window",
294        help = "Aggregate usage by server rate-limit windows: 5h or 7d"
295    )]
296    limit_window: Option<String>,
297    #[arg(
298        short = 'S',
299        long,
300        value_name = "sort",
301        help = "time, tokens, credits, calls, sessions"
302    )]
303    sort: Option<String>,
304    #[arg(short = 'n', long, value_name = "n", help = "Maximum rows to show")]
305    limit: Option<String>,
306    #[arg(
307        short = 'T',
308        long,
309        value_name = "n",
310        help = "Number of sessions to show"
311    )]
312    top: Option<String>,
313    #[arg(short = 'd', long, help = "Show full event-level rows")]
314    detail: bool,
315    #[arg(short = 'F', long, help = "Scan all session files")]
316    full_scan: bool,
317    #[arg(short = 'a', long, help = "Include all session usage")]
318    all: bool,
319    #[arg(short = 'r', long, help = "Include reasoning effort in model grouping")]
320    reasoning_effort: bool,
321    #[arg(
322        short = 'A',
323        long,
324        value_name = "id",
325        help = "Only include one account id"
326    )]
327    account_id: Option<String>,
328    #[arg(long, value_name = "path", help = "Path to auth.json")]
329    auth_file: Option<PathBuf>,
330    #[arg(long, value_name = "path", help = "Auth account history file")]
331    account_history_file: Option<PathBuf>,
332    #[arg(long, value_name = "path", help = "Codex home directory")]
333    codex_home: Option<PathBuf>,
334    #[arg(long, value_name = "path", help = "Codex sessions directory")]
335    sessions_dir: Option<PathBuf>,
336    #[arg(short = 's', long, value_name = "time", help = "Start time")]
337    start: Option<String>,
338    #[arg(short = 'e', long, value_name = "time", help = "End time")]
339    end: Option<String>,
340    #[arg(short = 't', long, help = "Use today as the range")]
341    today: bool,
342    #[arg(long, help = "Use yesterday as the range")]
343    yesterday: bool,
344    #[arg(short = 'm', long, help = "Use the current calendar month")]
345    month: bool,
346    #[arg(
347        short = 'L',
348        long,
349        value_name = "duration",
350        help = "Recent duration such as 12h, 7d, 2w, 1mo"
351    )]
352    last: Option<String>,
353    #[arg(
354        short = 'f',
355        long,
356        value_name = "format",
357        help = "table, json, csv, markdown"
358    )]
359    format: Option<String>,
360    #[arg(short = 'j', long, help = "Print JSON")]
361    json: bool,
362    #[arg(short = 'v', long, help = "Show diagnostics")]
363    verbose: bool,
364}
365
366#[derive(Debug, Args)]
367struct LimitArgs {
368    #[command(subcommand)]
369    command: Option<LimitSubcommand>,
370}
371
372#[derive(Debug, Subcommand)]
373enum LimitSubcommand {
374    #[command(
375        about = "Show latest observed rate-limit state",
376        override_usage = "codex-ops limit current [options]"
377    )]
378    Current(LimitCurrentArgs),
379    #[command(
380        about = "List observed rate-limit windows",
381        override_usage = "codex-ops limit windows [options]"
382    )]
383    Windows(LimitCommonArgs),
384    #[command(
385        about = "Show rate-limit used-percent change timeline",
386        override_usage = "codex-ops limit trend [options]"
387    )]
388    Trend(LimitCommonArgs),
389    #[command(
390        about = "Show detected rate-limit reset events",
391        override_usage = "codex-ops limit resets [options]"
392    )]
393    Resets(LimitResetArgs),
394    #[command(
395        about = "Export raw rate-limit samples",
396        override_usage = "codex-ops limit samples [options]"
397    )]
398    Samples(LimitCommonArgs),
399}
400
401#[derive(Debug, Args)]
402struct LimitResetArgs {
403    #[command(flatten)]
404    common: LimitCommonArgs,
405    #[arg(long, help = "Only include resets before the prior reset time")]
406    early_only: bool,
407}
408
409#[derive(Debug, Args)]
410struct LimitCurrentArgs {
411    #[arg(long, value_name = "window", help = "5h or 7d")]
412    window: Option<String>,
413    #[arg(
414        short = 'A',
415        long,
416        value_name = "id",
417        help = "Only include one account id"
418    )]
419    account_id: Option<String>,
420    #[arg(long, value_name = "path", help = "Path to auth.json")]
421    auth_file: Option<PathBuf>,
422    #[arg(long, value_name = "path", help = "Auth account history file")]
423    account_history_file: Option<PathBuf>,
424    #[arg(long, value_name = "path", help = "Codex home directory")]
425    codex_home: Option<PathBuf>,
426    #[arg(long, value_name = "path", help = "Codex sessions directory")]
427    sessions_dir: Option<PathBuf>,
428    #[arg(
429        short = 'f',
430        long,
431        value_name = "format",
432        help = "table, json, csv, markdown"
433    )]
434    format: Option<String>,
435    #[arg(short = 'j', long, help = "Print JSON")]
436    json: bool,
437    #[arg(short = 'v', long, help = "Show diagnostics")]
438    verbose: bool,
439}
440
441#[derive(Debug, Args)]
442struct LimitCommonArgs {
443    #[arg(long, value_name = "window", help = "5h or 7d")]
444    window: Option<String>,
445    #[arg(
446        short = 'A',
447        long,
448        value_name = "id",
449        help = "Only include one account id"
450    )]
451    account_id: Option<String>,
452    #[arg(long, value_name = "path", help = "Path to auth.json")]
453    auth_file: Option<PathBuf>,
454    #[arg(long, value_name = "path", help = "Auth account history file")]
455    account_history_file: Option<PathBuf>,
456    #[arg(long, value_name = "path", help = "Codex home directory")]
457    codex_home: Option<PathBuf>,
458    #[arg(long, value_name = "path", help = "Codex sessions directory")]
459    sessions_dir: Option<PathBuf>,
460    #[arg(short = 's', long, value_name = "time", help = "Start time")]
461    start: Option<String>,
462    #[arg(short = 'e', long, value_name = "time", help = "End time")]
463    end: Option<String>,
464    #[arg(
465        short = 'L',
466        long,
467        value_name = "duration",
468        help = "Recent duration such as 12h, 30d, 2w, 1mo"
469    )]
470    last: Option<String>,
471    #[arg(
472        short = 'f',
473        long,
474        value_name = "format",
475        help = "table, json, csv, markdown"
476    )]
477    format: Option<String>,
478    #[arg(short = 'j', long, help = "Print JSON")]
479    json: bool,
480    #[arg(short = 'v', long, help = "Show diagnostics")]
481    verbose: bool,
482}
483
484pub fn parse_cli(args: &[String]) -> Result<ParsedCli, CliParseError> {
485    let argv = std::iter::once("codex-ops".to_string()).chain(args.iter().cloned());
486    match CliArgs::try_parse_from(argv) {
487        Ok(parsed) => parsed.into_parsed_cli(),
488        Err(error) => match error.kind() {
489            ErrorKind::DisplayHelp => Ok(ParsedCli::Help(error.to_string())),
490            ErrorKind::DisplayVersion => Ok(ParsedCli::Version),
491            _ => Err(CliParseError::new(error.exit_code(), error.to_string())),
492        },
493    }
494}
495
496impl CliArgs {
497    fn into_parsed_cli(self) -> Result<ParsedCli, CliParseError> {
498        if self.version {
499            return Ok(ParsedCli::Version);
500        }
501
502        match self.command {
503            None => Ok(ParsedCli::Help(root_help())),
504            Some(RootCommand::Auth(args)) => auth_command(args),
505            Some(RootCommand::Doctor(args)) => Ok(parsed_command(CliCommand::Doctor(
506                DoctorCliCommand::Run(doctor_options(args)?),
507            ))),
508            Some(RootCommand::Stat(args)) => {
509                Ok(parsed_command(CliCommand::Stat(stat_command(args)?)))
510            }
511            Some(RootCommand::Limit(args)) => limit_command(args),
512        }
513    }
514}
515
516fn auth_command(args: AuthArgs) -> Result<ParsedCli, CliParseError> {
517    let command = match args.command {
518        None => return Ok(ParsedCli::Help(auth_help())),
519        Some(AuthCommand::Status(args)) => AuthCliCommand::Status(AuthStatusCliOptions {
520            paths: AuthCliPaths {
521                auth_file: resolve_cli_path(args.auth_file)?,
522                codex_home: args.codex_home,
523                ..AuthCliPaths::default()
524            },
525            json: args.json,
526            include_token_claims: args.include_token_claims,
527        }),
528        Some(AuthCommand::Save(args)) => AuthCliCommand::Save(AuthProfileCliOptions {
529            paths: auth_profile_paths(args)?,
530        }),
531        Some(AuthCommand::List(args)) => AuthCliCommand::List(AuthProfileCliOptions {
532            paths: auth_profile_paths(args)?,
533        }),
534        Some(AuthCommand::Select(args)) => AuthCliCommand::Select(AuthSelectCliOptions {
535            paths: AuthCliPaths {
536                auth_file: resolve_cli_path(args.auth_file)?,
537                codex_home: args.codex_home,
538                store_dir: resolve_cli_path(args.store_dir)?,
539                account_history_file: resolve_cli_path(args.account_history_file)?,
540            },
541            account_id: args.account_id,
542        }),
543        Some(AuthCommand::Remove(args)) => AuthCliCommand::Remove(AuthRemoveCliOptions {
544            paths: AuthCliPaths {
545                auth_file: resolve_cli_path(args.auth_file)?,
546                codex_home: args.codex_home,
547                store_dir: resolve_cli_path(args.store_dir)?,
548                account_history_file: None,
549            },
550            account_id: args.account_id,
551            yes: args.yes,
552        }),
553    };
554
555    Ok(parsed_command(CliCommand::Auth(command)))
556}
557
558fn auth_profile_paths(args: AuthProfileArgs) -> Result<AuthCliPaths, CliParseError> {
559    Ok(AuthCliPaths {
560        auth_file: resolve_cli_path(args.auth_file)?,
561        codex_home: args.codex_home,
562        store_dir: resolve_cli_path(args.store_dir)?,
563        account_history_file: None,
564    })
565}
566
567fn doctor_options(args: DoctorArgs) -> Result<DoctorCliOptions, CliParseError> {
568    Ok(DoctorCliOptions {
569        paths: DoctorCliPaths {
570            auth_file: resolve_cli_path(args.auth_file)?,
571            codex_home: args.codex_home,
572            sessions_dir: args.sessions_dir,
573        },
574        json: args.json,
575    })
576}
577
578fn stat_command(args: StatArgs) -> Result<StatCliCommand, CliParseError> {
579    Ok(StatCliCommand {
580        view: args.view,
581        session: args.session,
582        options: stat_options(args.options)?,
583    })
584}
585
586fn stat_options(args: StatOptionArgs) -> Result<StatCommandOptions, CliParseError> {
587    Ok(StatCommandOptions {
588        start: args.start,
589        end: args.end,
590        group_by: args.group_by,
591        limit_window: args.limit_window,
592        format: args.format,
593        codex_home: args.codex_home,
594        sessions_dir: args.sessions_dir,
595        auth_file: resolve_cli_path(args.auth_file)?,
596        account_history_file: resolve_cli_path(args.account_history_file)?,
597        today: args.today,
598        yesterday: args.yesterday,
599        month: args.month,
600        all: args.all,
601        reasoning_effort: args.reasoning_effort,
602        account_id: args.account_id,
603        last: args.last,
604        sort: args.sort,
605        limit: args.limit,
606        top: args.top,
607        detail: args.detail,
608        full_scan: args.full_scan,
609        verbose: args.verbose,
610        json: args.json,
611    })
612}
613
614fn limit_command(args: LimitArgs) -> Result<ParsedCli, CliParseError> {
615    let command = match args.command {
616        None => return Ok(ParsedCli::Help(limit_help())),
617        Some(LimitSubcommand::Current(args)) => LimitCliCommand {
618            command: LimitCommand::Current,
619            options: limit_current_options(args)?,
620        },
621        Some(LimitSubcommand::Windows(args)) => LimitCliCommand {
622            command: LimitCommand::Windows,
623            options: limit_options(args, None)?,
624        },
625        Some(LimitSubcommand::Trend(args)) => LimitCliCommand {
626            command: LimitCommand::Trend,
627            options: limit_options(args, None)?,
628        },
629        Some(LimitSubcommand::Resets(args)) => LimitCliCommand {
630            command: LimitCommand::Resets,
631            options: limit_options(args.common, Some(args.early_only))?,
632        },
633        Some(LimitSubcommand::Samples(args)) => LimitCliCommand {
634            command: LimitCommand::Samples,
635            options: limit_options(args, None)?,
636        },
637    };
638
639    Ok(parsed_command(CliCommand::Limit(command)))
640}
641
642fn limit_current_options(args: LimitCurrentArgs) -> Result<LimitCommandOptions, CliParseError> {
643    Ok(LimitCommandOptions {
644        start: None,
645        end: None,
646        last: None,
647        format: args.format,
648        codex_home: args.codex_home,
649        sessions_dir: args.sessions_dir,
650        auth_file: resolve_cli_path(args.auth_file)?,
651        account_history_file: resolve_cli_path(args.account_history_file)?,
652        account_id: args.account_id,
653        window: args.window,
654        early_only: false,
655        json: args.json,
656        verbose: args.verbose,
657    })
658}
659
660fn limit_options(
661    args: LimitCommonArgs,
662    early_only: Option<bool>,
663) -> Result<LimitCommandOptions, CliParseError> {
664    Ok(LimitCommandOptions {
665        start: args.start,
666        end: args.end,
667        last: args.last,
668        format: args.format,
669        codex_home: args.codex_home,
670        sessions_dir: args.sessions_dir,
671        auth_file: resolve_cli_path(args.auth_file)?,
672        account_history_file: resolve_cli_path(args.account_history_file)?,
673        account_id: args.account_id,
674        window: args.window,
675        early_only: early_only.unwrap_or(false),
676        json: args.json,
677        verbose: args.verbose,
678    })
679}
680
681fn resolve_cli_path(path: Option<PathBuf>) -> Result<Option<PathBuf>, CliParseError> {
682    match path {
683        None => Ok(None),
684        Some(path) if path.is_absolute() => Ok(Some(path)),
685        Some(path) => env::current_dir()
686            .map(|cwd| Some(cwd.join(path)))
687            .map_err(|error| CliParseError::new(1, error.to_string())),
688    }
689}
690
691fn parsed_command(command: CliCommand) -> ParsedCli {
692    ParsedCli::Command(Box::new(command))
693}
694
695fn root_help() -> String {
696    let mut command = CliArgs::command();
697    command.render_help().to_string()
698}
699
700fn auth_help() -> String {
701    let mut command = CliArgs::command();
702    let auth = command
703        .find_subcommand_mut("auth")
704        .expect("auth subcommand is defined");
705    auth.render_help().to_string()
706}
707
708fn limit_help() -> String {
709    let mut command = CliArgs::command();
710    let limit = command
711        .find_subcommand_mut("limit")
712        .expect("limit subcommand is defined");
713    limit.render_help().to_string()
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    #[test]
721    fn resolves_path_options_from_space_separated_flags() {
722        let args = vec![
723            "auth".to_string(),
724            "status".to_string(),
725            "--auth-file".to_string(),
726            "fixtures/auth.json".to_string(),
727        ];
728
729        let parsed = parse_cli(&args).expect("parse cli");
730        let ParsedCli::Command(command) = parsed else {
731            panic!("expected command");
732        };
733        let CliCommand::Auth(AuthCliCommand::Status(options)) = *command else {
734            panic!("expected auth status");
735        };
736
737        assert_eq!(
738            options.paths.auth_file,
739            Some(env::current_dir().expect("cwd").join("fixtures/auth.json"))
740        );
741    }
742
743    #[test]
744    fn resolves_path_options_from_equals_flags() {
745        let args = vec![
746            "limit".to_string(),
747            "samples".to_string(),
748            "--account-history-file=fixtures/history.json".to_string(),
749        ];
750
751        let parsed = parse_cli(&args).expect("parse cli");
752        let ParsedCli::Command(command) = parsed else {
753            panic!("expected command");
754        };
755        let CliCommand::Limit(LimitCliCommand { options, .. }) = *command else {
756            panic!("expected limit samples");
757        };
758        let cwd = env::current_dir().expect("cwd");
759
760        assert_eq!(
761            options.account_history_file,
762            Some(cwd.join("fixtures/history.json"))
763        );
764    }
765}