1use crate::Codex;
5use crate::command::CodexCommand;
6use crate::command::exec::effective_sandbox;
7use crate::error::Result;
8use crate::exec::{self, CommandOutput};
9use crate::types::{ApprovalPolicy, SandboxMode};
10
11#[derive(Debug, Clone)]
13pub struct ForkCommand {
14 approve_for_me: bool,
15 session_id: Option<String>,
16 prompt: Option<String>,
17 last: bool,
18 all: bool,
19 config_overrides: Vec<String>,
20 enabled_features: Vec<String>,
21 disabled_features: Vec<String>,
22 images: Vec<String>,
23 model: Option<String>,
24 oss: bool,
25 local_provider: Option<String>,
26 profile: Option<String>,
27 sandbox: Option<SandboxMode>,
28 approval_policy: Option<ApprovalPolicy>,
29 full_auto: bool,
30 dangerously_bypass_approvals_and_sandbox: bool,
31 dangerously_bypass_hook_trust: bool,
32 strict_config: bool,
33 no_alt_screen: bool,
34 remote: Option<String>,
35 remote_auth_token_env: Option<String>,
36 cd: Option<String>,
37 search: bool,
38 add_dirs: Vec<String>,
39}
40
41impl ForkCommand {
42 #[must_use]
43 pub fn new() -> Self {
44 Self {
45 approve_for_me: false,
46 session_id: None,
47 prompt: None,
48 last: false,
49 all: false,
50 config_overrides: Vec::new(),
51 enabled_features: Vec::new(),
52 disabled_features: Vec::new(),
53 images: Vec::new(),
54 model: None,
55 oss: false,
56 local_provider: None,
57 profile: None,
58 sandbox: None,
59 approval_policy: None,
60 full_auto: false,
61 dangerously_bypass_approvals_and_sandbox: false,
62 dangerously_bypass_hook_trust: false,
63 strict_config: false,
64 no_alt_screen: false,
65 remote: None,
66 remote_auth_token_env: None,
67 cd: None,
68 search: false,
69 add_dirs: Vec::new(),
70 }
71 }
72
73 #[must_use]
75 pub fn session_id(mut self, id: impl Into<String>) -> Self {
76 self.session_id = Some(id.into());
77 self
78 }
79
80 #[must_use]
82 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
83 self.prompt = Some(prompt.into());
84 self
85 }
86
87 #[must_use]
89 pub fn last(mut self) -> Self {
90 self.last = true;
91 self
92 }
93
94 #[must_use]
96 pub fn all(mut self) -> Self {
97 self.all = true;
98 self
99 }
100
101 #[must_use]
102 pub fn config(mut self, key_value: impl Into<String>) -> Self {
103 self.config_overrides.push(key_value.into());
104 self
105 }
106
107 #[must_use]
108 pub fn enable(mut self, feature: impl Into<String>) -> Self {
109 self.enabled_features.push(feature.into());
110 self
111 }
112
113 #[must_use]
114 pub fn disable(mut self, feature: impl Into<String>) -> Self {
115 self.disabled_features.push(feature.into());
116 self
117 }
118
119 #[must_use]
120 pub fn image(mut self, path: impl Into<String>) -> Self {
121 self.images.push(path.into());
122 self
123 }
124
125 #[must_use]
126 pub fn model(mut self, model: impl Into<String>) -> Self {
127 self.model = Some(model.into());
128 self
129 }
130
131 #[must_use]
132 pub fn oss(mut self) -> Self {
133 self.oss = true;
134 self
135 }
136
137 #[must_use]
138 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
139 self.local_provider = Some(provider.into());
140 self
141 }
142
143 #[must_use]
144 pub fn profile(mut self, profile: impl Into<String>) -> Self {
145 self.profile = Some(profile.into());
146 self
147 }
148
149 #[must_use]
150 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
151 self.sandbox = Some(sandbox);
152 self
153 }
154
155 #[must_use]
156 pub fn approval_policy(mut self, policy: ApprovalPolicy) -> Self {
157 self.approval_policy = Some(policy);
158 self
159 }
160
161 #[must_use]
168 pub fn full_auto(mut self) -> Self {
169 self.full_auto = true;
170 self
171 }
172
173 #[must_use]
181 pub fn approve_for_me(mut self) -> Self {
182 self.approve_for_me = true;
183 self
184 }
185
186 #[must_use]
187 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
188 self.dangerously_bypass_approvals_and_sandbox = true;
189 self
190 }
191
192 #[must_use]
194 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
195 self.dangerously_bypass_hook_trust = true;
196 self
197 }
198
199 #[must_use]
201 pub fn strict_config(mut self) -> Self {
202 self.strict_config = true;
203 self
204 }
205
206 #[must_use]
208 pub fn no_alt_screen(mut self) -> Self {
209 self.no_alt_screen = true;
210 self
211 }
212
213 #[must_use]
215 pub fn remote(mut self, addr: impl Into<String>) -> Self {
216 self.remote = Some(addr.into());
217 self
218 }
219
220 #[must_use]
223 pub fn remote_auth_token_env(mut self, env_var: impl Into<String>) -> Self {
224 self.remote_auth_token_env = Some(env_var.into());
225 self
226 }
227
228 #[must_use]
229 pub fn cd(mut self, dir: impl Into<String>) -> Self {
230 self.cd = Some(dir.into());
231 self
232 }
233
234 #[must_use]
236 pub fn search(mut self) -> Self {
237 self.search = true;
238 self
239 }
240
241 #[must_use]
242 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
243 self.add_dirs.push(dir.into());
244 self
245 }
246}
247
248impl Default for ForkCommand {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254impl CodexCommand for ForkCommand {
255 type Output = CommandOutput;
256
257 fn args(&self) -> Vec<String> {
258 let mut args = vec!["fork".into()];
259
260 for v in &self.config_overrides {
261 args.push("-c".into());
262 args.push(v.clone());
263 }
264 for v in &self.enabled_features {
265 args.push("--enable".into());
266 args.push(v.clone());
267 }
268 for v in &self.disabled_features {
269 args.push("--disable".into());
270 args.push(v.clone());
271 }
272 if self.last {
273 args.push("--last".into());
274 }
275 if self.all {
276 args.push("--all".into());
277 }
278 for v in &self.images {
279 args.push("--image".into());
280 args.push(v.clone());
281 }
282 if let Some(model) = &self.model {
283 args.push("--model".into());
284 args.push(model.clone());
285 }
286 if self.oss {
287 args.push("--oss".into());
288 }
289 if let Some(provider) = &self.local_provider {
290 args.push("--local-provider".into());
291 args.push(provider.clone());
292 }
293 if let Some(profile) = &self.profile {
294 args.push("--profile".into());
295 args.push(profile.clone());
296 }
297 if let Some(sandbox) = effective_sandbox(self.sandbox, self.full_auto) {
298 args.push("--sandbox".into());
299 args.push(sandbox.as_arg().into());
300 }
301 if let Some(policy) = self.approval_policy {
302 args.push("--ask-for-approval".into());
303 args.push(policy.as_arg().into());
304 }
305 if self.approve_for_me {
306 args.push("--approve-for-me".into());
307 }
308 if self.dangerously_bypass_approvals_and_sandbox {
309 args.push("--dangerously-bypass-approvals-and-sandbox".into());
310 }
311 if self.dangerously_bypass_hook_trust {
312 args.push("--dangerously-bypass-hook-trust".into());
313 }
314 if self.strict_config {
315 args.push("--strict-config".into());
316 }
317 if self.no_alt_screen {
318 args.push("--no-alt-screen".into());
319 }
320 if let Some(remote) = &self.remote {
321 args.push("--remote".into());
322 args.push(remote.clone());
323 }
324 if let Some(env_var) = &self.remote_auth_token_env {
325 args.push("--remote-auth-token-env".into());
326 args.push(env_var.clone());
327 }
328 if let Some(cd) = &self.cd {
329 args.push("--cd".into());
330 args.push(cd.clone());
331 }
332 if self.search {
333 args.push("--search".into());
334 }
335 for v in &self.add_dirs {
336 args.push("--add-dir".into());
337 args.push(v.clone());
338 }
339 if let Some(id) = &self.session_id {
340 args.push(id.clone());
341 }
342 if let Some(prompt) = &self.prompt {
343 args.push(prompt.clone());
344 }
345 args
346 }
347
348 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
349 exec::run_codex(codex, self.args()).await
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn fork_last_args() {
359 let args = ForkCommand::new()
360 .last()
361 .model("gpt-5")
362 .prompt("take a different approach")
363 .args();
364 assert_eq!(
365 args,
366 vec![
367 "fork",
368 "--last",
369 "--model",
370 "gpt-5",
371 "take a different approach"
372 ]
373 );
374 }
375
376 #[test]
377 fn fork_session_id_args() {
378 let args = ForkCommand::new()
379 .session_id("abc-123")
380 .full_auto()
381 .search()
382 .args();
383 assert_eq!(
384 args,
385 vec![
386 "fork",
387 "--sandbox",
388 "workspace-write",
389 "--search",
390 "abc-123"
391 ]
392 );
393 }
394
395 #[test]
398 fn fork_full_auto_never_emits_the_flag() {
399 let args = ForkCommand::new().full_auto().args();
400 assert!(!args.iter().any(|a| a == "--full-auto"));
401 }
402
403 #[test]
404 fn fork_explicit_sandbox_wins_over_full_auto() {
405 let args = ForkCommand::new()
406 .full_auto()
407 .sandbox(SandboxMode::ReadOnly)
408 .args();
409 assert_eq!(args, vec!["fork", "--sandbox", "read-only"]);
410 }
411
412 #[test]
413 fn fork_new_flags_args() {
414 let args = ForkCommand::new()
415 .last()
416 .strict_config()
417 .set_bypass_hook_trust()
418 .no_alt_screen()
419 .remote("ws://host:9000")
420 .remote_auth_token_env("CODEX_TOKEN")
421 .args();
422 assert_eq!(
423 args,
424 vec![
425 "fork",
426 "--last",
427 "--dangerously-bypass-hook-trust",
428 "--strict-config",
429 "--no-alt-screen",
430 "--remote",
431 "ws://host:9000",
432 "--remote-auth-token-env",
433 "CODEX_TOKEN",
434 ]
435 );
436 }
437}