1use std::ffi::OsString;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum Mode {
10 Text,
12 #[default]
14 Json,
15 Rpc,
17}
18
19impl Mode {
20 fn as_str(self) -> &'static str {
21 match self {
22 Mode::Text => "text",
23 Mode::Json => "json",
24 Mode::Rpc => "rpc",
25 }
26 }
27}
28
29#[derive(Debug, Clone, Default)]
40pub struct PiCliBuilder {
41 mode: Mode,
42 provider: Option<String>,
43 model: Option<String>,
44 api_key: Option<String>,
45 system_prompt: Option<String>,
46 session: Option<String>,
47 session_dir: Option<PathBuf>,
48 session_id: Option<String>,
49 fork: Option<String>,
50 continue_session: bool,
51 no_session: bool,
52 name: Option<String>,
53 no_tools: bool,
54 tools: Option<String>,
55 exclude_tools: Option<String>,
56 thinking: Option<String>,
57 print: bool,
58 approve: bool,
59 no_approve: bool,
60 offline: bool,
61 no_mcp: bool,
62 working_directory: Option<PathBuf>,
63 extra_args: Vec<OsString>,
64 prompt: Option<String>,
65}
66
67impl PiCliBuilder {
68 pub fn new() -> Self {
69 Self::default()
70 }
71
72 pub fn mode(mut self, mode: Mode) -> Self {
73 self.mode = mode;
74 self
75 }
76
77 pub fn provider(mut self, provider: impl Into<String>) -> Self {
81 self.provider = Some(provider.into());
82 self
83 }
84
85 pub fn model(mut self, model: impl Into<String>) -> Self {
86 self.model = Some(model.into());
87 self
88 }
89
90 pub fn api_key(mut self, key: impl Into<String>) -> Self {
93 self.api_key = Some(key.into());
94 self
95 }
96
97 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
98 self.system_prompt = Some(prompt.into());
99 self
100 }
101
102 pub fn session(mut self, session: impl Into<String>) -> Self {
104 self.session = Some(session.into());
105 self
106 }
107
108 pub fn session_dir(mut self, dir: impl Into<PathBuf>) -> Self {
109 self.session_dir = Some(dir.into());
110 self
111 }
112
113 pub fn session_id(mut self, id: impl Into<String>) -> Self {
115 self.session_id = Some(id.into());
116 self
117 }
118
119 pub fn fork(mut self, source: impl Into<String>) -> Self {
121 self.fork = Some(source.into());
122 self
123 }
124
125 pub fn continue_session(mut self, enabled: bool) -> Self {
127 self.continue_session = enabled;
128 self
129 }
130
131 pub fn no_session(mut self, enabled: bool) -> Self {
133 self.no_session = enabled;
134 self
135 }
136
137 pub fn name(mut self, name: impl Into<String>) -> Self {
139 self.name = Some(name.into());
140 self
141 }
142
143 pub fn no_tools(mut self, enabled: bool) -> Self {
145 self.no_tools = enabled;
146 self
147 }
148
149 pub fn tools(mut self, tools: impl Into<String>) -> Self {
153 self.tools = Some(tools.into());
154 self
155 }
156
157 pub fn exclude_tools(mut self, tools: impl Into<String>) -> Self {
160 self.exclude_tools = Some(tools.into());
161 self
162 }
163
164 pub fn thinking(mut self, level: impl Into<String>) -> Self {
166 self.thinking = Some(level.into());
167 self
168 }
169
170 pub fn print(mut self, enabled: bool) -> Self {
172 self.print = enabled;
173 self
174 }
175
176 pub fn approve(mut self, enabled: bool) -> Self {
181 self.approve = enabled;
182 self
183 }
184
185 pub fn no_approve(mut self, enabled: bool) -> Self {
187 self.no_approve = enabled;
188 self
189 }
190
191 pub fn offline(mut self, enabled: bool) -> Self {
193 self.offline = enabled;
194 self
195 }
196
197 pub fn no_mcp(mut self, enabled: bool) -> Self {
200 self.no_mcp = enabled;
201 self
202 }
203
204 pub fn working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
208 self.working_directory = Some(dir.into());
209 self
210 }
211
212 pub fn get_working_directory(&self) -> Option<&std::path::Path> {
214 self.working_directory.as_deref()
215 }
216
217 pub fn extra_args<I, S>(mut self, args: I) -> Self
220 where
221 I: IntoIterator<Item = S>,
222 S: Into<OsString>,
223 {
224 self.extra_args.extend(args.into_iter().map(Into::into));
225 self
226 }
227
228 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
230 self.prompt = Some(prompt.into());
231 self
232 }
233
234 pub fn assembled_args(&self) -> Vec<OsString> {
236 let mut a: Vec<OsString> = vec!["--mode".into(), self.mode.as_str().into()];
237 let mut flag = |name: &str, v: &Option<String>| {
238 if let Some(v) = v {
239 a.push(name.into());
240 a.push(v.into());
241 }
242 };
243 flag("--provider", &self.provider);
244 flag("--model", &self.model);
245 flag("--api-key", &self.api_key);
246 flag("--system-prompt", &self.system_prompt);
247 flag("--session", &self.session);
248 flag("--session-id", &self.session_id);
249 flag("--fork", &self.fork);
250 flag("--name", &self.name);
251 flag("--tools", &self.tools);
252 flag("--exclude-tools", &self.exclude_tools);
253 flag("--thinking", &self.thinking);
254 if let Some(dir) = &self.session_dir {
255 a.push("--session-dir".into());
256 a.push(dir.into());
257 }
258 if self.continue_session {
259 a.push("--continue".into());
260 }
261 if self.no_session {
262 a.push("--no-session".into());
263 }
264 if self.no_tools {
265 a.push("--no-tools".into());
266 }
267 if self.print {
268 a.push("--print".into());
269 }
270 if self.approve {
271 a.push("--approve".into());
272 }
273 if self.no_approve {
274 a.push("--no-approve".into());
275 }
276 if self.offline {
277 a.push("--offline".into());
278 }
279 if self.no_mcp {
280 a.push("--no-mcp".into());
281 }
282 a.extend(self.extra_args.iter().cloned());
283 if let Some(p) = &self.prompt {
284 a.push(p.into());
285 }
286 a
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn assembled_args_order_flags_then_extras_then_prompt() {
296 let args = PiCliBuilder::new()
297 .mode(Mode::Json)
298 .provider("anthropic")
299 .no_session(true)
300 .extra_args(["--verbose"])
301 .prompt("hello")
302 .assembled_args();
303 let s: Vec<String> = args
304 .iter()
305 .map(|a| a.to_string_lossy().into_owned())
306 .collect();
307 assert_eq!(s[0..2], ["--mode", "json"]);
308 assert!(s.contains(&"--no-session".to_string()));
309 let vi = s.iter().position(|x| x == "--verbose").unwrap();
310 assert_eq!(s.last().unwrap(), "hello");
311 assert!(vi < s.len() - 1, "extras precede the positional prompt");
312 }
313
314 #[test]
315 fn rpc_mode_default_has_no_prompt() {
316 let args = PiCliBuilder::new().mode(Mode::Rpc).assembled_args();
317 assert_eq!(args.len(), 2);
318 assert_eq!(args[1], "rpc");
319 }
320
321 #[test]
322 fn no_mcp_flag() {
323 let args = PiCliBuilder::new().no_mcp(true).assembled_args();
324 assert_eq!(args.last().unwrap(), "--no-mcp");
325 let args = PiCliBuilder::new().no_mcp(false).assembled_args();
326 assert!(!args.iter().any(|a| a == "--no-mcp"));
327 }
328}