Skip to main content

codex_wrapper/command/
fork.rs

1/// Fork a previous interactive session.
2///
3/// Wraps `codex fork [session-id] [prompt]`.
4use crate::Codex;
5use crate::command::CodexCommand;
6use crate::error::Result;
7use crate::exec::{self, CommandOutput};
8use crate::types::{ApprovalPolicy, SandboxMode};
9
10/// Fork a previous interactive Codex session, creating a new branch of conversation.
11#[derive(Debug, Clone)]
12pub struct ForkCommand {
13    session_id: Option<String>,
14    prompt: Option<String>,
15    last: bool,
16    all: bool,
17    config_overrides: Vec<String>,
18    enabled_features: Vec<String>,
19    disabled_features: Vec<String>,
20    images: Vec<String>,
21    model: Option<String>,
22    oss: bool,
23    local_provider: Option<String>,
24    profile: Option<String>,
25    sandbox: Option<SandboxMode>,
26    approval_policy: Option<ApprovalPolicy>,
27    full_auto: bool,
28    dangerously_bypass_approvals_and_sandbox: bool,
29    cd: Option<String>,
30    search: bool,
31    add_dirs: Vec<String>,
32}
33
34impl ForkCommand {
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            session_id: None,
39            prompt: None,
40            last: false,
41            all: false,
42            config_overrides: Vec::new(),
43            enabled_features: Vec::new(),
44            disabled_features: Vec::new(),
45            images: Vec::new(),
46            model: None,
47            oss: false,
48            local_provider: None,
49            profile: None,
50            sandbox: None,
51            approval_policy: None,
52            full_auto: false,
53            dangerously_bypass_approvals_and_sandbox: false,
54            cd: None,
55            search: false,
56            add_dirs: Vec::new(),
57        }
58    }
59
60    /// Session ID (UUID) to fork.
61    #[must_use]
62    pub fn session_id(mut self, id: impl Into<String>) -> Self {
63        self.session_id = Some(id.into());
64        self
65    }
66
67    /// Optional prompt to start the forked session with.
68    #[must_use]
69    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
70        self.prompt = Some(prompt.into());
71        self
72    }
73
74    /// Fork the most recent session without showing the picker.
75    #[must_use]
76    pub fn last(mut self) -> Self {
77        self.last = true;
78        self
79    }
80
81    /// Show all sessions (disables cwd filtering).
82    #[must_use]
83    pub fn all(mut self) -> Self {
84        self.all = true;
85        self
86    }
87
88    #[must_use]
89    pub fn config(mut self, key_value: impl Into<String>) -> Self {
90        self.config_overrides.push(key_value.into());
91        self
92    }
93
94    #[must_use]
95    pub fn enable(mut self, feature: impl Into<String>) -> Self {
96        self.enabled_features.push(feature.into());
97        self
98    }
99
100    #[must_use]
101    pub fn disable(mut self, feature: impl Into<String>) -> Self {
102        self.disabled_features.push(feature.into());
103        self
104    }
105
106    #[must_use]
107    pub fn image(mut self, path: impl Into<String>) -> Self {
108        self.images.push(path.into());
109        self
110    }
111
112    #[must_use]
113    pub fn model(mut self, model: impl Into<String>) -> Self {
114        self.model = Some(model.into());
115        self
116    }
117
118    #[must_use]
119    pub fn oss(mut self) -> Self {
120        self.oss = true;
121        self
122    }
123
124    #[must_use]
125    pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
126        self.local_provider = Some(provider.into());
127        self
128    }
129
130    #[must_use]
131    pub fn profile(mut self, profile: impl Into<String>) -> Self {
132        self.profile = Some(profile.into());
133        self
134    }
135
136    #[must_use]
137    pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
138        self.sandbox = Some(sandbox);
139        self
140    }
141
142    #[must_use]
143    pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
144        self.approval_policy = Some(policy);
145        self
146    }
147
148    #[must_use]
149    pub fn full_auto(mut self) -> Self {
150        self.full_auto = true;
151        self
152    }
153
154    #[must_use]
155    pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
156        self.dangerously_bypass_approvals_and_sandbox = true;
157        self
158    }
159
160    #[must_use]
161    pub fn cd(mut self, dir: impl Into<String>) -> Self {
162        self.cd = Some(dir.into());
163        self
164    }
165
166    /// Enable live web search.
167    #[must_use]
168    pub fn search(mut self) -> Self {
169        self.search = true;
170        self
171    }
172
173    #[must_use]
174    pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
175        self.add_dirs.push(dir.into());
176        self
177    }
178}
179
180impl Default for ForkCommand {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186impl CodexCommand for ForkCommand {
187    type Output = CommandOutput;
188
189    fn args(&self) -> Vec<String> {
190        let mut args = vec!["fork".into()];
191
192        for v in &self.config_overrides {
193            args.push("-c".into());
194            args.push(v.clone());
195        }
196        for v in &self.enabled_features {
197            args.push("--enable".into());
198            args.push(v.clone());
199        }
200        for v in &self.disabled_features {
201            args.push("--disable".into());
202            args.push(v.clone());
203        }
204        if self.last {
205            args.push("--last".into());
206        }
207        if self.all {
208            args.push("--all".into());
209        }
210        for v in &self.images {
211            args.push("--image".into());
212            args.push(v.clone());
213        }
214        if let Some(model) = &self.model {
215            args.push("--model".into());
216            args.push(model.clone());
217        }
218        if self.oss {
219            args.push("--oss".into());
220        }
221        if let Some(provider) = &self.local_provider {
222            args.push("--local-provider".into());
223            args.push(provider.clone());
224        }
225        if let Some(profile) = &self.profile {
226            args.push("--profile".into());
227            args.push(profile.clone());
228        }
229        if let Some(sandbox) = self.sandbox {
230            args.push("--sandbox".into());
231            args.push(sandbox.as_arg().into());
232        }
233        if let Some(policy) = self.approval_policy {
234            args.push("--ask-for-approval".into());
235            args.push(policy.as_arg().into());
236        }
237        if self.full_auto {
238            args.push("--full-auto".into());
239        }
240        if self.dangerously_bypass_approvals_and_sandbox {
241            args.push("--dangerously-bypass-approvals-and-sandbox".into());
242        }
243        if let Some(cd) = &self.cd {
244            args.push("--cd".into());
245            args.push(cd.clone());
246        }
247        if self.search {
248            args.push("--search".into());
249        }
250        for v in &self.add_dirs {
251            args.push("--add-dir".into());
252            args.push(v.clone());
253        }
254        if let Some(id) = &self.session_id {
255            args.push(id.clone());
256        }
257        if let Some(prompt) = &self.prompt {
258            args.push(prompt.clone());
259        }
260        args
261    }
262
263    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
264        exec::run_codex(codex, self.args()).await
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn fork_last_args() {
274        let args = ForkCommand::new()
275            .last()
276            .model("gpt-5")
277            .prompt("take a different approach")
278            .args();
279        assert_eq!(
280            args,
281            vec![
282                "fork",
283                "--last",
284                "--model",
285                "gpt-5",
286                "take a different approach"
287            ]
288        );
289    }
290
291    #[test]
292    fn fork_session_id_args() {
293        let args = ForkCommand::new()
294            .session_id("abc-123")
295            .full_auto()
296            .search()
297            .args();
298        assert_eq!(args, vec!["fork", "--full-auto", "--search", "abc-123"]);
299    }
300}