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    dangerously_bypass_hook_trust: bool,
30    strict_config: bool,
31    no_alt_screen: bool,
32    remote: Option<String>,
33    remote_auth_token_env: Option<String>,
34    cd: Option<String>,
35    search: bool,
36    add_dirs: Vec<String>,
37}
38
39impl ForkCommand {
40    #[must_use]
41    pub fn new() -> Self {
42        Self {
43            session_id: None,
44            prompt: None,
45            last: false,
46            all: false,
47            config_overrides: Vec::new(),
48            enabled_features: Vec::new(),
49            disabled_features: Vec::new(),
50            images: Vec::new(),
51            model: None,
52            oss: false,
53            local_provider: None,
54            profile: None,
55            sandbox: None,
56            approval_policy: None,
57            full_auto: false,
58            dangerously_bypass_approvals_and_sandbox: false,
59            dangerously_bypass_hook_trust: false,
60            strict_config: false,
61            no_alt_screen: false,
62            remote: None,
63            remote_auth_token_env: None,
64            cd: None,
65            search: false,
66            add_dirs: Vec::new(),
67        }
68    }
69
70    /// Session ID (UUID) to fork.
71    #[must_use]
72    pub fn session_id(mut self, id: impl Into<String>) -> Self {
73        self.session_id = Some(id.into());
74        self
75    }
76
77    /// Optional prompt to start the forked session with.
78    #[must_use]
79    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
80        self.prompt = Some(prompt.into());
81        self
82    }
83
84    /// Fork the most recent session without showing the picker.
85    #[must_use]
86    pub fn last(mut self) -> Self {
87        self.last = true;
88        self
89    }
90
91    /// Show all sessions (disables cwd filtering).
92    #[must_use]
93    pub fn all(mut self) -> Self {
94        self.all = true;
95        self
96    }
97
98    #[must_use]
99    pub fn config(mut self, key_value: impl Into<String>) -> Self {
100        self.config_overrides.push(key_value.into());
101        self
102    }
103
104    #[must_use]
105    pub fn enable(mut self, feature: impl Into<String>) -> Self {
106        self.enabled_features.push(feature.into());
107        self
108    }
109
110    #[must_use]
111    pub fn disable(mut self, feature: impl Into<String>) -> Self {
112        self.disabled_features.push(feature.into());
113        self
114    }
115
116    #[must_use]
117    pub fn image(mut self, path: impl Into<String>) -> Self {
118        self.images.push(path.into());
119        self
120    }
121
122    #[must_use]
123    pub fn model(mut self, model: impl Into<String>) -> Self {
124        self.model = Some(model.into());
125        self
126    }
127
128    #[must_use]
129    pub fn oss(mut self) -> Self {
130        self.oss = true;
131        self
132    }
133
134    #[must_use]
135    pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
136        self.local_provider = Some(provider.into());
137        self
138    }
139
140    #[must_use]
141    pub fn profile(mut self, profile: impl Into<String>) -> Self {
142        self.profile = Some(profile.into());
143        self
144    }
145
146    #[must_use]
147    pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
148        self.sandbox = Some(sandbox);
149        self
150    }
151
152    #[must_use]
153    pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
154        self.approval_policy = Some(policy);
155        self
156    }
157
158    #[must_use]
159    pub fn full_auto(mut self) -> Self {
160        self.full_auto = true;
161        self
162    }
163
164    #[must_use]
165    pub fn dangerously_bypass_approvals_and_sandbox(mut self) -> Self {
166        self.dangerously_bypass_approvals_and_sandbox = true;
167        self
168    }
169
170    /// Bypass the hook trust prompt (`--dangerously-bypass-hook-trust`).
171    #[must_use]
172    pub fn dangerously_bypass_hook_trust(mut self) -> Self {
173        self.dangerously_bypass_hook_trust = true;
174        self
175    }
176
177    /// Error on unrecognized config keys (`--strict-config`).
178    #[must_use]
179    pub fn strict_config(mut self) -> Self {
180        self.strict_config = true;
181        self
182    }
183
184    /// Do not use the terminal alternate screen (`--no-alt-screen`).
185    #[must_use]
186    pub fn no_alt_screen(mut self) -> Self {
187        self.no_alt_screen = true;
188        self
189    }
190
191    /// Connect the TUI to a remote app server endpoint (`--remote <ADDR>`).
192    #[must_use]
193    pub fn remote(mut self, addr: impl Into<String>) -> Self {
194        self.remote = Some(addr.into());
195        self
196    }
197
198    /// Env var holding the bearer token for the remote app server
199    /// (`--remote-auth-token-env <ENV_VAR>`).
200    #[must_use]
201    pub fn remote_auth_token_env(mut self, env_var: impl Into<String>) -> Self {
202        self.remote_auth_token_env = Some(env_var.into());
203        self
204    }
205
206    #[must_use]
207    pub fn cd(mut self, dir: impl Into<String>) -> Self {
208        self.cd = Some(dir.into());
209        self
210    }
211
212    /// Enable live web search.
213    #[must_use]
214    pub fn search(mut self) -> Self {
215        self.search = true;
216        self
217    }
218
219    #[must_use]
220    pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
221        self.add_dirs.push(dir.into());
222        self
223    }
224}
225
226impl Default for ForkCommand {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232impl CodexCommand for ForkCommand {
233    type Output = CommandOutput;
234
235    fn args(&self) -> Vec<String> {
236        let mut args = vec!["fork".into()];
237
238        for v in &self.config_overrides {
239            args.push("-c".into());
240            args.push(v.clone());
241        }
242        for v in &self.enabled_features {
243            args.push("--enable".into());
244            args.push(v.clone());
245        }
246        for v in &self.disabled_features {
247            args.push("--disable".into());
248            args.push(v.clone());
249        }
250        if self.last {
251            args.push("--last".into());
252        }
253        if self.all {
254            args.push("--all".into());
255        }
256        for v in &self.images {
257            args.push("--image".into());
258            args.push(v.clone());
259        }
260        if let Some(model) = &self.model {
261            args.push("--model".into());
262            args.push(model.clone());
263        }
264        if self.oss {
265            args.push("--oss".into());
266        }
267        if let Some(provider) = &self.local_provider {
268            args.push("--local-provider".into());
269            args.push(provider.clone());
270        }
271        if let Some(profile) = &self.profile {
272            args.push("--profile".into());
273            args.push(profile.clone());
274        }
275        if let Some(sandbox) = self.sandbox {
276            args.push("--sandbox".into());
277            args.push(sandbox.as_arg().into());
278        }
279        if let Some(policy) = self.approval_policy {
280            args.push("--ask-for-approval".into());
281            args.push(policy.as_arg().into());
282        }
283        if self.full_auto {
284            args.push("--full-auto".into());
285        }
286        if self.dangerously_bypass_approvals_and_sandbox {
287            args.push("--dangerously-bypass-approvals-and-sandbox".into());
288        }
289        if self.dangerously_bypass_hook_trust {
290            args.push("--dangerously-bypass-hook-trust".into());
291        }
292        if self.strict_config {
293            args.push("--strict-config".into());
294        }
295        if self.no_alt_screen {
296            args.push("--no-alt-screen".into());
297        }
298        if let Some(remote) = &self.remote {
299            args.push("--remote".into());
300            args.push(remote.clone());
301        }
302        if let Some(env_var) = &self.remote_auth_token_env {
303            args.push("--remote-auth-token-env".into());
304            args.push(env_var.clone());
305        }
306        if let Some(cd) = &self.cd {
307            args.push("--cd".into());
308            args.push(cd.clone());
309        }
310        if self.search {
311            args.push("--search".into());
312        }
313        for v in &self.add_dirs {
314            args.push("--add-dir".into());
315            args.push(v.clone());
316        }
317        if let Some(id) = &self.session_id {
318            args.push(id.clone());
319        }
320        if let Some(prompt) = &self.prompt {
321            args.push(prompt.clone());
322        }
323        args
324    }
325
326    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
327        exec::run_codex(codex, self.args()).await
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn fork_last_args() {
337        let args = ForkCommand::new()
338            .last()
339            .model("gpt-5")
340            .prompt("take a different approach")
341            .args();
342        assert_eq!(
343            args,
344            vec![
345                "fork",
346                "--last",
347                "--model",
348                "gpt-5",
349                "take a different approach"
350            ]
351        );
352    }
353
354    #[test]
355    fn fork_session_id_args() {
356        let args = ForkCommand::new()
357            .session_id("abc-123")
358            .full_auto()
359            .search()
360            .args();
361        assert_eq!(args, vec!["fork", "--full-auto", "--search", "abc-123"]);
362    }
363
364    #[test]
365    fn fork_new_flags_args() {
366        let args = ForkCommand::new()
367            .last()
368            .strict_config()
369            .dangerously_bypass_hook_trust()
370            .no_alt_screen()
371            .remote("ws://host:9000")
372            .remote_auth_token_env("CODEX_TOKEN")
373            .args();
374        assert_eq!(
375            args,
376            vec![
377                "fork",
378                "--last",
379                "--dangerously-bypass-hook-trust",
380                "--strict-config",
381                "--no-alt-screen",
382                "--remote",
383                "ws://host:9000",
384                "--remote-auth-token-env",
385                "CODEX_TOKEN",
386            ]
387        );
388    }
389}