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