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 working_directory: Option<PathBuf>,
62 extra_args: Vec<OsString>,
63 prompt: Option<String>,
64}
65
66impl PiCliBuilder {
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn mode(mut self, mode: Mode) -> Self {
72 self.mode = mode;
73 self
74 }
75
76 pub fn provider(mut self, provider: impl Into<String>) -> Self {
77 self.provider = Some(provider.into());
78 self
79 }
80
81 pub fn model(mut self, model: impl Into<String>) -> Self {
82 self.model = Some(model.into());
83 self
84 }
85
86 pub fn api_key(mut self, key: impl Into<String>) -> Self {
89 self.api_key = Some(key.into());
90 self
91 }
92
93 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
94 self.system_prompt = Some(prompt.into());
95 self
96 }
97
98 pub fn session(mut self, session: impl Into<String>) -> Self {
100 self.session = Some(session.into());
101 self
102 }
103
104 pub fn session_dir(mut self, dir: impl Into<PathBuf>) -> Self {
105 self.session_dir = Some(dir.into());
106 self
107 }
108
109 pub fn session_id(mut self, id: impl Into<String>) -> Self {
111 self.session_id = Some(id.into());
112 self
113 }
114
115 pub fn fork(mut self, source: impl Into<String>) -> Self {
117 self.fork = Some(source.into());
118 self
119 }
120
121 pub fn continue_session(mut self, enabled: bool) -> Self {
123 self.continue_session = enabled;
124 self
125 }
126
127 pub fn no_session(mut self, enabled: bool) -> Self {
129 self.no_session = enabled;
130 self
131 }
132
133 pub fn name(mut self, name: impl Into<String>) -> Self {
135 self.name = Some(name.into());
136 self
137 }
138
139 pub fn no_tools(mut self, enabled: bool) -> Self {
141 self.no_tools = enabled;
142 self
143 }
144
145 pub fn tools(mut self, tools: impl Into<String>) -> Self {
147 self.tools = Some(tools.into());
148 self
149 }
150
151 pub fn exclude_tools(mut self, tools: impl Into<String>) -> Self {
153 self.exclude_tools = Some(tools.into());
154 self
155 }
156
157 pub fn thinking(mut self, level: impl Into<String>) -> Self {
159 self.thinking = Some(level.into());
160 self
161 }
162
163 pub fn print(mut self, enabled: bool) -> Self {
165 self.print = enabled;
166 self
167 }
168
169 pub fn approve(mut self, enabled: bool) -> Self {
174 self.approve = enabled;
175 self
176 }
177
178 pub fn no_approve(mut self, enabled: bool) -> Self {
180 self.no_approve = enabled;
181 self
182 }
183
184 pub fn offline(mut self, enabled: bool) -> Self {
186 self.offline = enabled;
187 self
188 }
189
190 pub fn working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
194 self.working_directory = Some(dir.into());
195 self
196 }
197
198 pub fn get_working_directory(&self) -> Option<&std::path::Path> {
200 self.working_directory.as_deref()
201 }
202
203 pub fn extra_args<I, S>(mut self, args: I) -> Self
206 where
207 I: IntoIterator<Item = S>,
208 S: Into<OsString>,
209 {
210 self.extra_args.extend(args.into_iter().map(Into::into));
211 self
212 }
213
214 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
216 self.prompt = Some(prompt.into());
217 self
218 }
219
220 pub fn assembled_args(&self) -> Vec<OsString> {
222 let mut a: Vec<OsString> = vec!["--mode".into(), self.mode.as_str().into()];
223 let mut flag = |name: &str, v: &Option<String>| {
224 if let Some(v) = v {
225 a.push(name.into());
226 a.push(v.into());
227 }
228 };
229 flag("--provider", &self.provider);
230 flag("--model", &self.model);
231 flag("--api-key", &self.api_key);
232 flag("--system-prompt", &self.system_prompt);
233 flag("--session", &self.session);
234 flag("--session-id", &self.session_id);
235 flag("--fork", &self.fork);
236 flag("--name", &self.name);
237 flag("--tools", &self.tools);
238 flag("--exclude-tools", &self.exclude_tools);
239 flag("--thinking", &self.thinking);
240 if let Some(dir) = &self.session_dir {
241 a.push("--session-dir".into());
242 a.push(dir.into());
243 }
244 if self.continue_session {
245 a.push("--continue".into());
246 }
247 if self.no_session {
248 a.push("--no-session".into());
249 }
250 if self.no_tools {
251 a.push("--no-tools".into());
252 }
253 if self.print {
254 a.push("--print".into());
255 }
256 if self.approve {
257 a.push("--approve".into());
258 }
259 if self.no_approve {
260 a.push("--no-approve".into());
261 }
262 if self.offline {
263 a.push("--offline".into());
264 }
265 a.extend(self.extra_args.iter().cloned());
266 if let Some(p) = &self.prompt {
267 a.push(p.into());
268 }
269 a
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn assembled_args_order_flags_then_extras_then_prompt() {
279 let args = PiCliBuilder::new()
280 .mode(Mode::Json)
281 .provider("anthropic")
282 .no_session(true)
283 .extra_args(["--verbose"])
284 .prompt("hello")
285 .assembled_args();
286 let s: Vec<String> = args
287 .iter()
288 .map(|a| a.to_string_lossy().into_owned())
289 .collect();
290 assert_eq!(s[0..2], ["--mode", "json"]);
291 assert!(s.contains(&"--no-session".to_string()));
292 let vi = s.iter().position(|x| x == "--verbose").unwrap();
293 assert_eq!(s.last().unwrap(), "hello");
294 assert!(vi < s.len() - 1, "extras precede the positional prompt");
295 }
296
297 #[test]
298 fn rpc_mode_default_has_no_prompt() {
299 let args = PiCliBuilder::new().mode(Mode::Rpc).assembled_args();
300 assert_eq!(args.len(), 2);
301 assert_eq!(args[1], "rpc");
302 }
303}