1use crate::contracts::{
18 GitPublishMode, GitRevertMode, Runner, RunnerApprovalMode, RunnerCliOptionsPatch,
19 RunnerOutputFormat, RunnerPlanMode, RunnerSandboxMode, RunnerVerbosity,
20 UnsupportedOptionPolicy,
21};
22use anyhow::{Result, anyhow, bail};
23
24use super::args::RunnerCliArgs;
25
26pub fn parse_runner(value: &str) -> Result<Runner> {
28 let normalized = value.trim().to_lowercase();
29 match normalized.as_str() {
30 "codex" => Ok(Runner::Codex),
31 "opencode" => Ok(Runner::Opencode),
32 "gemini" => Ok(Runner::Gemini),
33 "claude" => Ok(Runner::Claude),
34 "cursor" => Ok(Runner::Cursor),
35 "kimi" => Ok(Runner::Kimi),
36 "pi" => Ok(Runner::Pi),
37 _ => bail!(
38 "Invalid runner: --runner must be 'codex', 'opencode', 'gemini', 'claude', 'cursor', 'kimi', or 'pi' (got: {}). Set a supported runner in .ralph/config.jsonc or via the --runner flag.",
39 value.trim()
40 ),
41 }
42}
43
44pub fn parse_git_revert_mode(value: &str) -> Result<GitRevertMode> {
46 value.parse().map_err(|err: &str| anyhow!(err))
47}
48
49pub fn parse_git_publish_mode(value: &str) -> Result<GitPublishMode> {
51 value.parse().map_err(|err: &str| anyhow!(err))
52}
53
54pub(crate) fn parse_runner_cli_patch(args: &RunnerCliArgs) -> Result<RunnerCliOptionsPatch> {
56 let output_format = match args.output_format.as_deref() {
57 Some(value) => Some(
58 value
59 .parse::<RunnerOutputFormat>()
60 .map_err(|err| anyhow!(err))?,
61 ),
62 None => None,
63 };
64 let verbosity = match args.verbosity.as_deref() {
65 Some(value) => Some(
66 value
67 .parse::<RunnerVerbosity>()
68 .map_err(|err| anyhow!(err))?,
69 ),
70 None => None,
71 };
72 let approval_mode = match args.approval_mode.as_deref() {
73 Some(value) => Some(
74 value
75 .parse::<RunnerApprovalMode>()
76 .map_err(|err| anyhow!(err))?,
77 ),
78 None => None,
79 };
80 let sandbox = match args.sandbox.as_deref() {
81 Some(value) => Some(
82 value
83 .parse::<RunnerSandboxMode>()
84 .map_err(|err| anyhow!(err))?,
85 ),
86 None => None,
87 };
88 let plan_mode = match args.plan_mode.as_deref() {
89 Some(value) => Some(
90 value
91 .parse::<RunnerPlanMode>()
92 .map_err(|err| anyhow!(err))?,
93 ),
94 None => None,
95 };
96 let unsupported_option_policy = match args.unsupported_option_policy.as_deref() {
97 Some(value) => Some(
98 value
99 .parse::<UnsupportedOptionPolicy>()
100 .map_err(|err| anyhow!(err))?,
101 ),
102 None => None,
103 };
104
105 Ok(RunnerCliOptionsPatch {
106 output_format,
107 verbosity,
108 approval_mode,
109 sandbox,
110 plan_mode,
111 unsupported_option_policy,
112 })
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn parse_runner_accepts_valid_runners() {
121 assert!(matches!(parse_runner("codex"), Ok(Runner::Codex)));
122 assert!(matches!(parse_runner("opencode"), Ok(Runner::Opencode)));
123 assert!(matches!(parse_runner("gemini"), Ok(Runner::Gemini)));
124 assert!(matches!(parse_runner("claude"), Ok(Runner::Claude)));
125 assert!(matches!(parse_runner("cursor"), Ok(Runner::Cursor)));
126 assert!(matches!(parse_runner("kimi"), Ok(Runner::Kimi)));
127 assert!(matches!(parse_runner("pi"), Ok(Runner::Pi)));
128 assert!(matches!(parse_runner("CODEX"), Ok(Runner::Codex)));
129 }
130
131 #[test]
132 fn parse_runner_rejects_invalid_runners() {
133 assert!(parse_runner("invalid").is_err());
134 assert!(parse_runner("").is_err());
135 }
136}