use std::ffi::OsString;
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use serde::{Deserialize, Serialize};
use crate::{CliError, CliResult};
#[derive(Clone, Debug, Parser)]
#[command(name = "starweaver-cli", version, about = "Starweaver local CLI")]
pub struct Cli {
#[arg(short = 'p', long = "prompt", global = false)]
pub prompt: Option<String>,
#[arg(
short = 's',
long,
global = false,
conflicts_with_all = ["new_session", "continue_session"]
)]
pub session: Option<String>,
#[arg(long = "continue", global = false, conflicts_with = "new_session")]
pub continue_session: bool,
#[arg(long, global = false)]
pub new_session: bool,
#[arg(long, global = false)]
pub run: Option<String>,
#[arg(long, global = false, conflicts_with = "run")]
pub branch_from: Option<String>,
#[arg(long, global = false)]
pub profile: Option<String>,
#[arg(long, global = false, num_args = 0..=1, default_missing_value = "true")]
pub worker: Option<String>,
#[arg(long = "worker-label", global = false)]
pub worker_label: Option<String>,
#[arg(
short = 'w',
long,
global = false,
num_args = 0..=1,
default_missing_value = "true"
)]
pub worktree: Option<String>,
#[arg(long = "worktree-name", global = false)]
pub worktree_name: Option<String>,
#[arg(long, global = false)]
pub branch: Option<String>,
#[arg(long, global = false)]
pub output: Option<OutputMode>,
#[arg(long, global = false)]
pub hitl: Option<HitlPolicy>,
#[arg(long, global = true)]
pub store: Option<String>,
#[command(subcommand)]
pub command: Option<CliCommand>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Subcommand)]
pub enum CliCommand {
Version,
Run(RunCommand),
Session {
#[command(subcommand)]
command: SessionCommand,
},
Profile {
#[command(subcommand)]
command: ProfileCommand,
},
Setup(SetupCommand),
Auth {
#[command(subcommand)]
command: AuthCommand,
},
Skill {
#[command(subcommand)]
command: CatalogCommand,
},
Subagent {
#[command(subcommand)]
command: CatalogCommand,
},
Mcp {
#[command(subcommand)]
command: CatalogCommand,
},
Tools {
#[command(subcommand)]
command: ToolsCommand,
},
Tui(TuiCommand),
Approval {
#[command(subcommand)]
command: ApprovalCommand,
},
Deferred {
#[command(subcommand)]
command: DeferredCommand,
},
Resume(ResumeCommand),
Reset(ResetCommand),
Diagnostics,
Rpc(RpcCommand),
ReplayCheck,
Update(UpdateCommand),
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Completion {
shell: Shell,
},
}
#[derive(Clone, Debug, Args)]
pub struct RunCommand {
#[arg(short = 'p', long = "prompt")]
pub prompt: Option<String>,
pub prompt_parts: Vec<String>,
#[arg(short = 's', conflicts_with_all = ["new_session", "continue_session"], long)]
pub session: Option<String>,
#[arg(long = "continue", conflicts_with = "new_session")]
pub continue_session: bool,
#[arg(long)]
pub new_session: bool,
#[arg(long)]
pub run: Option<String>,
#[arg(long, conflicts_with = "run")]
pub branch_from: Option<String>,
#[arg(long)]
pub profile: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
pub worker: Option<String>,
#[arg(long = "worker-label")]
pub worker_label: Option<String>,
#[arg(short = 'w', long, num_args = 0..=1, default_missing_value = "true")]
pub worktree: Option<String>,
#[arg(long = "worktree-name")]
pub worktree_name: Option<String>,
#[arg(long)]
pub branch: Option<String>,
#[arg(long)]
pub output: Option<OutputMode>,
#[arg(long)]
pub hitl: Option<HitlPolicy>,
#[arg(skip)]
pub goal: Option<GoalCommandOptions>,
#[arg(skip)]
pub session_affinity_id: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GoalCommandOptions {
pub objective: String,
pub max_iterations: usize,
}
impl RunCommand {
pub fn prompt_text(&self) -> CliResult<String> {
let prompt = self
.prompt
.clone()
.unwrap_or_else(|| self.prompt_parts.join(" "));
if prompt.trim().is_empty() {
Err(CliError::Usage(
"usage: starweaver-cli run -p <prompt>".to_string(),
))
} else {
Ok(prompt)
}
}
}
#[derive(Clone, Debug, Subcommand)]
pub enum SessionCommand {
List(SessionListCommand),
Show(SessionShowCommand),
Replay(SessionReplayCommand),
Delete(SessionDeleteCommand),
Trim(SessionTrimCommand),
}
#[derive(Clone, Debug, Args)]
pub struct SessionListCommand {
#[arg(long, default_value = "display-jsonl")]
pub output: OutputMode,
#[arg(long, default_value_t = 50)]
pub limit: usize,
}
#[derive(Clone, Debug, Args)]
pub struct SessionShowCommand {
pub session_id: String,
#[arg(long, default_value = "display-jsonl")]
pub output: OutputMode,
#[arg(long, default_value_t = 20)]
pub runs: usize,
}
#[derive(Clone, Debug, Args)]
pub struct SessionReplayCommand {
pub session_id: String,
#[arg(long)]
pub run: Option<String>,
#[arg(long)]
pub after: Option<usize>,
#[arg(long, default_value = "display-jsonl")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct SessionDeleteCommand {
pub session_id: String,
#[arg(long)]
pub yes: bool,
#[arg(long, default_value = "text")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct SessionTrimCommand {
#[arg(long)]
pub current: bool,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub session: Option<String>,
#[arg(long, default_value_t = 20)]
pub keep_runs: usize,
#[arg(long)]
pub older_than: Option<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long, default_value = "display-jsonl")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Subcommand)]
pub enum ProfileCommand {
List,
Show { name: String },
}
#[derive(Clone, Debug, Args)]
pub struct SetupCommand {
#[arg(long, conflicts_with = "project")]
pub global: bool,
#[arg(long)]
pub project: bool,
#[arg(long)]
pub force: bool,
}
#[derive(Clone, Debug, Subcommand)]
pub enum AuthCommand {
Login(AuthProviderCommand),
Status(AuthStatusCommand),
Refresh(AuthProviderCommand),
Logout(AuthLogoutCommand),
Doctor(AuthDoctorCommand),
}
#[derive(Clone, Debug, Args)]
pub struct AuthProviderCommand {
#[arg(default_value = "codex", value_parser = ["codex"])]
pub provider: String,
#[arg(long = "auth-file")]
pub auth_file: Option<String>,
#[arg(long, default_value_t = 15 * 60)]
pub timeout_seconds: u64,
}
#[derive(Clone, Debug, Args)]
pub struct AuthStatusCommand {
#[arg(default_value = "codex", value_parser = ["codex"])]
pub provider: Option<String>,
#[arg(long = "auth-file")]
pub auth_file: Option<String>,
}
#[derive(Clone, Debug, Args)]
pub struct AuthLogoutCommand {
#[arg(default_value = "codex", value_parser = ["codex"])]
pub provider: String,
#[arg(long = "auth-file")]
pub auth_file: Option<String>,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub revoke: bool,
}
#[derive(Clone, Debug, Args)]
pub struct AuthDoctorCommand {
#[arg(long = "auth-file")]
pub auth_file: Option<String>,
}
#[derive(Clone, Debug, Subcommand)]
pub enum CatalogCommand {
List,
Show { name: String },
Doctor,
}
#[derive(Clone, Debug, Subcommand)]
pub enum ToolsCommand {
List,
Doctor,
}
#[derive(Clone, Debug, Args)]
pub struct TuiCommand {
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub run: Option<String>,
#[arg(long)]
pub after: Option<usize>,
#[arg(long)]
pub interactive: bool,
#[arg(long, conflicts_with = "interactive")]
pub snapshot: bool,
#[arg(long, default_value = "text")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Subcommand)]
pub enum ApprovalCommand {
List(ApprovalListCommand),
Show { approval_id: String },
Approve(ApprovalDecisionCommand),
Reject(ApprovalDecisionCommand),
}
#[derive(Clone, Debug, Args)]
pub struct ApprovalListCommand {
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub run: Option<String>,
#[arg(long, default_value = "display-jsonl")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct ApprovalDecisionCommand {
pub approval_id: String,
#[arg(long)]
pub reason: Option<String>,
#[arg(long, default_value = "text")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Subcommand)]
pub enum DeferredCommand {
List(DeferredListCommand),
Show { deferred_id: String },
Complete(DeferredCompleteCommand),
Fail(DeferredFailCommand),
}
#[derive(Clone, Debug, Args)]
pub struct DeferredListCommand {
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub run: Option<String>,
#[arg(long, default_value = "display-jsonl")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct DeferredCompleteCommand {
pub deferred_id: String,
#[arg(long)]
pub result: String,
#[arg(long, default_value = "text")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct DeferredFailCommand {
pub deferred_id: String,
#[arg(long)]
pub error: String,
#[arg(long, default_value = "text")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct ResumeCommand {
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub run: Option<String>,
#[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
pub prompt: String,
#[arg(long)]
pub output: Option<OutputMode>,
#[arg(long)]
pub hitl: Option<HitlPolicy>,
}
#[derive(Clone, Debug, Args)]
pub struct ResetCommand {
#[arg(long)]
pub yes: bool,
#[arg(long, default_value = "text")]
pub output: OutputMode,
}
#[derive(Clone, Debug, Args)]
pub struct RpcCommand {
#[arg(default_value = "stdio")]
pub transport: RpcTransport,
#[arg(long, default_value = "127.0.0.1")]
pub host: String,
#[arg(long, default_value_t = 8765)]
pub port: u16,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum RpcTransport {
#[default]
Stdio,
Http,
}
#[derive(Clone, Debug, Args)]
pub struct UpdateCommand {
#[arg(default_value = "cli")]
pub target: String,
#[arg(long)]
pub dry_run: bool,
#[arg(long, short = 'f')]
pub force: bool,
}
#[derive(Clone, Debug, Subcommand)]
pub enum ConfigCommand {
Init {
#[arg(long, conflicts_with = "project")]
global: bool,
#[arg(long)]
project: bool,
#[arg(long)]
force: bool,
},
Get { key: String },
Set {
#[arg(long, conflicts_with = "project")]
global: bool,
#[arg(long)]
project: bool,
key: String,
value: String,
},
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum OutputMode {
Text,
#[default]
DisplayJsonl,
AguiJsonl,
Json,
Silent,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
#[serde(rename_all = "snake_case")]
pub enum HitlPolicy {
Deny,
Defer,
Fail,
#[default]
Prompt,
}
#[must_use]
pub fn command() -> clap::Command {
Cli::command()
}
pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
Cli::try_parse_from(args).map_err(|error| clap_error(&error))
}
#[allow(dead_code)]
pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
Cli::try_parse_from(args).map_err(|error| clap_error(&error))
}
fn clap_error(error: &clap::Error) -> CliError {
match error.kind() {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
CliError::Display(error.to_string())
}
_ => CliError::Usage(error.to_string()),
}
}