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 extra_args: Vec<OsString>,
59 prompt: Option<String>,
60}
61
62impl PiCliBuilder {
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 pub fn mode(mut self, mode: Mode) -> Self {
68 self.mode = mode;
69 self
70 }
71
72 pub fn provider(mut self, provider: impl Into<String>) -> Self {
73 self.provider = Some(provider.into());
74 self
75 }
76
77 pub fn model(mut self, model: impl Into<String>) -> Self {
78 self.model = Some(model.into());
79 self
80 }
81
82 pub fn api_key(mut self, key: impl Into<String>) -> Self {
85 self.api_key = Some(key.into());
86 self
87 }
88
89 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
90 self.system_prompt = Some(prompt.into());
91 self
92 }
93
94 pub fn session(mut self, session: impl Into<String>) -> Self {
96 self.session = Some(session.into());
97 self
98 }
99
100 pub fn session_dir(mut self, dir: impl Into<PathBuf>) -> Self {
101 self.session_dir = Some(dir.into());
102 self
103 }
104
105 pub fn session_id(mut self, id: impl Into<String>) -> Self {
107 self.session_id = Some(id.into());
108 self
109 }
110
111 pub fn fork(mut self, source: impl Into<String>) -> Self {
113 self.fork = Some(source.into());
114 self
115 }
116
117 pub fn continue_session(mut self, enabled: bool) -> Self {
119 self.continue_session = enabled;
120 self
121 }
122
123 pub fn no_session(mut self, enabled: bool) -> Self {
125 self.no_session = enabled;
126 self
127 }
128
129 pub fn name(mut self, name: impl Into<String>) -> Self {
131 self.name = Some(name.into());
132 self
133 }
134
135 pub fn no_tools(mut self, enabled: bool) -> Self {
137 self.no_tools = enabled;
138 self
139 }
140
141 pub fn tools(mut self, tools: impl Into<String>) -> Self {
143 self.tools = Some(tools.into());
144 self
145 }
146
147 pub fn exclude_tools(mut self, tools: impl Into<String>) -> Self {
149 self.exclude_tools = Some(tools.into());
150 self
151 }
152
153 pub fn thinking(mut self, level: impl Into<String>) -> Self {
155 self.thinking = Some(level.into());
156 self
157 }
158
159 pub fn print(mut self, enabled: bool) -> Self {
161 self.print = enabled;
162 self
163 }
164
165 pub fn extra_args<I, S>(mut self, args: I) -> Self
168 where
169 I: IntoIterator<Item = S>,
170 S: Into<OsString>,
171 {
172 self.extra_args.extend(args.into_iter().map(Into::into));
173 self
174 }
175
176 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
178 self.prompt = Some(prompt.into());
179 self
180 }
181
182 pub fn assembled_args(&self) -> Vec<OsString> {
184 let mut a: Vec<OsString> = vec!["--mode".into(), self.mode.as_str().into()];
185 let mut flag = |name: &str, v: &Option<String>| {
186 if let Some(v) = v {
187 a.push(name.into());
188 a.push(v.into());
189 }
190 };
191 flag("--provider", &self.provider);
192 flag("--model", &self.model);
193 flag("--api-key", &self.api_key);
194 flag("--system-prompt", &self.system_prompt);
195 flag("--session", &self.session);
196 flag("--session-id", &self.session_id);
197 flag("--fork", &self.fork);
198 flag("--name", &self.name);
199 flag("--tools", &self.tools);
200 flag("--exclude-tools", &self.exclude_tools);
201 flag("--thinking", &self.thinking);
202 if let Some(dir) = &self.session_dir {
203 a.push("--session-dir".into());
204 a.push(dir.into());
205 }
206 if self.continue_session {
207 a.push("--continue".into());
208 }
209 if self.no_session {
210 a.push("--no-session".into());
211 }
212 if self.no_tools {
213 a.push("--no-tools".into());
214 }
215 if self.print {
216 a.push("--print".into());
217 }
218 a.extend(self.extra_args.iter().cloned());
219 if let Some(p) = &self.prompt {
220 a.push(p.into());
221 }
222 a
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn assembled_args_order_flags_then_extras_then_prompt() {
232 let args = PiCliBuilder::new()
233 .mode(Mode::Json)
234 .provider("anthropic")
235 .no_session(true)
236 .extra_args(["--verbose"])
237 .prompt("hello")
238 .assembled_args();
239 let s: Vec<String> = args
240 .iter()
241 .map(|a| a.to_string_lossy().into_owned())
242 .collect();
243 assert_eq!(s[0..2], ["--mode", "json"]);
244 assert!(s.contains(&"--no-session".to_string()));
245 let vi = s.iter().position(|x| x == "--verbose").unwrap();
246 assert_eq!(s.last().unwrap(), "hello");
247 assert!(vi < s.len() - 1, "extras precede the positional prompt");
248 }
249
250 #[test]
251 fn rpc_mode_default_has_no_prompt() {
252 let args = PiCliBuilder::new().mode(Mode::Rpc).assembled_args();
253 assert_eq!(args.len(), 2);
254 assert_eq!(args[1], "rpc");
255 }
256}