use core::fmt;
use std::ffi::OsString;
use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OutputFormat {
Text,
Json,
#[default]
StreamJson,
}
impl OutputFormat {
pub fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Json => "json",
Self::StreamJson => "stream-json",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum InputFormat {
Text,
StreamJson,
}
impl InputFormat {
pub fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::StreamJson => "stream-json",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PermissionMode {
Default,
AcceptEdits,
Auto,
BypassPermissions,
Manual,
Plan,
DontAsk,
Other(String),
}
impl PermissionMode {
pub fn as_str(&self) -> &str {
match self {
Self::Default => "default",
Self::AcceptEdits => "acceptEdits",
Self::Auto => "auto",
Self::BypassPermissions => "bypassPermissions",
Self::Manual => "manual",
Self::Plan => "plan",
Self::DontAsk => "dontAsk",
Self::Other(s) => s,
}
}
}
impl From<&str> for PermissionMode {
fn from(s: &str) -> Self {
match s {
"default" => Self::Default,
"acceptEdits" => Self::AcceptEdits,
"auto" => Self::Auto,
"bypassPermissions" => Self::BypassPermissions,
"manual" => Self::Manual,
"plan" => Self::Plan,
"dontAsk" => Self::DontAsk,
_ => Self::Other(s.to_owned()),
}
}
}
impl From<String> for PermissionMode {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
impl fmt::Display for PermissionMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum SessionStart {
#[default]
New,
WithId(String),
Resume(String),
Continue,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Options {
input_format: Option<InputFormat>,
output_format: OutputFormat,
verbose: Option<bool>,
include_partial_messages: bool,
include_hook_events: bool,
model: Option<String>,
fallback_model: Option<String>,
effort: Option<String>,
restricted: bool,
permission_mode: Option<PermissionMode>,
permission_prompts: Option<String>,
permission_prompt_tool: Option<String>,
strict_mcp_config: bool,
mcp_config: Vec<OsString>,
tools: Option<Vec<String>>,
allowed_tools: Vec<String>,
disallowed_tools: Vec<String>,
disable_slash_commands: bool,
add_dirs: Vec<OsString>,
system_prompt: Option<String>,
append_system_prompt: Option<String>,
name: Option<String>,
setting_sources: Option<Vec<String>>,
settings: Map<String, Value>,
settings_file: Option<OsString>,
session: SessionStart,
fork_session: bool,
fork_session_id: Option<String>,
no_session_persistence: bool,
max_turns: Option<u32>,
max_budget_usd: Option<f64>,
extra_args: Vec<OsString>,
prompt_arg: Option<OsString>,
}
impl Options {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn input_format(mut self, format: InputFormat) -> Self {
self.input_format = Some(format);
self
}
#[must_use]
pub fn output_format(mut self, format: OutputFormat) -> Self {
self.output_format = format;
self
}
#[must_use]
pub fn verbose(mut self, on: bool) -> Self {
self.verbose = Some(on);
self
}
#[must_use]
pub fn include_partial_messages(mut self, on: bool) -> Self {
self.include_partial_messages = on;
self
}
#[must_use]
pub fn include_hook_events(mut self, on: bool) -> Self {
self.include_hook_events = on;
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
self.fallback_model = Some(model.into());
self
}
#[must_use]
pub fn effort(mut self, level: impl Into<String>) -> Self {
self.effort = Some(level.into());
self
}
#[must_use]
pub fn restricted(mut self, on: bool) -> Self {
self.restricted = on;
self
}
#[must_use]
pub fn permission_mode(mut self, mode: impl Into<PermissionMode>) -> Self {
self.permission_mode = Some(mode.into());
self
}
#[must_use]
pub fn permission_prompts(mut self, target: impl Into<String>) -> Self {
self.permission_prompts = Some(target.into());
self
}
#[must_use]
pub fn permission_prompt_tool(mut self, tool: impl Into<String>) -> Self {
self.permission_prompt_tool = Some(tool.into());
self
}
#[must_use]
pub fn strict_mcp_config(mut self, on: bool) -> Self {
self.strict_mcp_config = on;
self
}
#[must_use]
pub fn mcp_config(mut self, config: impl Into<OsString>) -> Self {
self.mcp_config.push(config.into());
self
}
#[must_use]
pub fn tools<I, S>(mut self, tools: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tools = Some(tools.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn allowed_tools<I, S>(mut self, tools: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_tools.extend(tools.into_iter().map(Into::into));
self
}
#[must_use]
pub fn disallowed_tools<I, S>(mut self, tools: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.disallowed_tools
.extend(tools.into_iter().map(Into::into));
self
}
#[must_use]
pub fn disable_slash_commands(mut self, on: bool) -> Self {
self.disable_slash_commands = on;
self
}
#[must_use]
pub fn add_dir(mut self, dir: impl Into<OsString>) -> Self {
self.add_dirs.push(dir.into());
self
}
#[must_use]
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
#[must_use]
pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.append_system_prompt = Some(prompt.into());
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn setting_sources<I, S>(mut self, sources: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.setting_sources = Some(sources.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn settings(mut self, settings: Map<String, Value>) -> Self {
self.settings.extend(settings);
self
}
#[must_use]
pub fn setting(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.settings.insert(key.into(), value.into());
self
}
#[must_use]
pub fn setting_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let env = self
.settings
.entry("env")
.or_insert_with(|| Value::Object(Map::new()));
if !env.is_object() {
*env = Value::Object(Map::new());
}
if let Value::Object(env) = env {
env.insert(key.into(), Value::String(value.into()));
}
self
}
#[must_use]
pub fn settings_file(mut self, path: impl Into<OsString>) -> Self {
self.settings_file = Some(path.into());
self
}
#[must_use]
pub fn resume(mut self, session_id: impl Into<String>) -> Self {
self.session = SessionStart::Resume(session_id.into());
self
}
#[must_use]
pub fn continue_last(mut self) -> Self {
self.session = SessionStart::Continue;
self
}
#[must_use]
pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
self.session = SessionStart::WithId(session_id.into());
self
}
#[must_use]
pub fn session(mut self, start: SessionStart) -> Self {
self.session = start;
self
}
#[must_use]
pub fn fork_session(mut self, on: bool) -> Self {
self.fork_session = on;
if !on {
self.fork_session_id = None;
}
self
}
#[must_use]
pub fn fork_session_id(mut self, session_id: impl Into<String>) -> Self {
self.fork_session = true;
self.fork_session_id = Some(session_id.into());
self
}
#[must_use]
pub fn no_session_persistence(mut self, on: bool) -> Self {
self.no_session_persistence = on;
self
}
#[must_use]
pub fn max_turns(mut self, turns: u32) -> Self {
self.max_turns = Some(turns);
self
}
#[must_use]
pub fn max_budget_usd(mut self, usd: f64) -> Self {
assert!(
usd.is_finite() && usd >= 0.0,
"max_budget_usd: {usd} is not a finite, non-negative amount"
);
self.max_budget_usd = Some(usd);
self
}
#[must_use]
pub fn extra_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
self.extra_args.extend(args.into_iter().map(Into::into));
self
}
#[must_use]
pub fn prompt_arg(mut self, prompt: impl Into<OsString>) -> Self {
self.prompt_arg = Some(prompt.into());
self
}
pub fn settings_json(&self) -> Option<String> {
(!self.settings.is_empty()).then(|| Value::Object(self.settings.clone()).to_string())
}
pub fn to_argv(&self) -> Vec<OsString> {
let mut a = Argv(Vec::new());
a.flag("-p");
if let Some(f) = self.input_format {
a.value("--input-format", f.as_str());
}
a.value("--output-format", self.output_format.as_str());
if self
.verbose
.unwrap_or(self.output_format == OutputFormat::StreamJson)
{
a.flag("--verbose");
}
a.switch("--include-partial-messages", self.include_partial_messages);
a.switch("--include-hook-events", self.include_hook_events);
a.opt("--model", self.model.as_deref());
a.opt("--fallback-model", self.fallback_model.as_deref());
a.opt("--effort", self.effort.as_deref());
a.switch("--restricted", self.restricted);
a.opt(
"--permission-mode",
self.permission_mode.as_ref().map(PermissionMode::as_str),
);
a.opt("--permission-prompts", self.permission_prompts.as_deref());
a.opt(
"--permission-prompt-tool",
self.permission_prompt_tool.as_deref(),
);
a.switch("--strict-mcp-config", self.strict_mcp_config);
for c in &self.mcp_config {
a.value("--mcp-config", c);
}
if let Some(t) = &self.tools {
a.value("--tools", t.join(","));
}
if !self.allowed_tools.is_empty() {
a.value("--allowedTools", self.allowed_tools.join(","));
}
if !self.disallowed_tools.is_empty() {
a.value("--disallowedTools", self.disallowed_tools.join(","));
}
a.switch("--disable-slash-commands", self.disable_slash_commands);
for d in &self.add_dirs {
a.value("--add-dir", d);
}
a.opt("--system-prompt", self.system_prompt.as_deref());
a.opt(
"--append-system-prompt",
self.append_system_prompt.as_deref(),
);
a.opt("--name", self.name.as_deref());
if let Some(s) = &self.setting_sources {
a.value("--setting-sources", s.join(","));
}
if let Some(s) = self.settings_json() {
a.value("--settings", s);
} else if let Some(f) = &self.settings_file {
a.value("--settings", f);
}
let resumed = match &self.session {
SessionStart::New => false,
SessionStart::WithId(id) => {
a.joined("--session-id", id);
false
}
SessionStart::Resume(id) => {
a.joined("--resume", id);
true
}
SessionStart::Continue => {
a.flag("--continue");
true
}
};
if resumed && self.fork_session {
a.flag("--fork-session");
if let Some(id) = &self.fork_session_id {
a.joined("--session-id", id);
}
}
a.switch("--no-session-persistence", self.no_session_persistence);
if let Some(n) = self.max_turns {
a.value("--max-turns", n.to_string());
}
if let Some(usd) = self.max_budget_usd {
a.value("--max-budget-usd", format!("{usd:.2}"));
}
a.0.extend(self.extra_args.iter().cloned());
if let Some(p) = &self.prompt_arg {
a.flag("--");
a.0.push(p.clone());
}
a.0
}
}
struct Argv(Vec<OsString>);
impl Argv {
fn flag(&mut self, flag: &str) {
self.0.push(flag.into());
}
fn switch(&mut self, flag: &str, on: bool) {
if on {
self.flag(flag);
}
}
fn value(&mut self, flag: &str, value: impl Into<OsString>) {
let value = value.into();
if value.as_encoded_bytes().starts_with(b"-") {
self.joined(flag, value);
} else {
self.0.extend([flag.into(), value]);
}
}
fn opt(&mut self, flag: &str, value: Option<&str>) {
if let Some(v) = value {
self.value(flag, v);
}
}
fn joined(&mut self, flag: &str, value: impl Into<OsString>) {
let mut a = OsString::from(flag);
a.push("=");
a.push(value.into());
self.0.push(a);
}
}
#[cfg(test)]
mod tests;