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}