1use std::ffi::OsString;
4
5use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
6use clap_complete::Shell;
7use serde::{Deserialize, Serialize};
8
9use crate::{CliError, CliResult};
10
11#[derive(Clone, Debug, Parser)]
13#[command(name = "starweaver-cli", version, about = "Starweaver local CLI")]
14pub struct Cli {
15 #[arg(short = 'p', long = "prompt", global = false)]
17 pub prompt: Option<String>,
18 #[arg(
20 short = 's',
21 long,
22 global = false,
23 conflicts_with_all = ["new_session", "continue_session"]
24 )]
25 pub session: Option<String>,
26 #[arg(long = "continue", global = false, conflicts_with = "new_session")]
28 pub continue_session: bool,
29 #[arg(long, global = false)]
31 pub new_session: bool,
32 #[arg(long, global = false)]
34 pub run: Option<String>,
35 #[arg(long, global = false, conflicts_with = "run")]
37 pub branch_from: Option<String>,
38 #[arg(long, global = false)]
40 pub profile: Option<String>,
41 #[arg(long, global = false, num_args = 0..=1, default_missing_value = "true")]
43 pub worker: Option<String>,
44 #[arg(long = "worker-label", global = false)]
46 pub worker_label: Option<String>,
47 #[arg(
49 short = 'w',
50 long,
51 global = false,
52 num_args = 0..=1,
53 default_missing_value = "true"
54 )]
55 pub worktree: Option<String>,
56 #[arg(long = "worktree-name", global = false)]
58 pub worktree_name: Option<String>,
59 #[arg(long, global = false)]
61 pub branch: Option<String>,
62 #[arg(long, global = false)]
64 pub output: Option<OutputMode>,
65 #[arg(long, global = false)]
67 pub hitl: Option<HitlPolicy>,
68 #[arg(long, global = true)]
70 pub store: Option<String>,
71 #[command(subcommand)]
73 pub command: Option<CliCommand>,
74}
75
76#[allow(clippy::large_enum_variant)]
78#[derive(Clone, Debug, Subcommand)]
79pub enum CliCommand {
80 Version,
82 Run(RunCommand),
84 Session {
86 #[command(subcommand)]
88 command: SessionCommand,
89 },
90 Profile {
92 #[command(subcommand)]
94 command: ProfileCommand,
95 },
96 Setup(SetupCommand),
98 Auth {
100 #[command(subcommand)]
102 command: AuthCommand,
103 },
104 Skill {
106 #[command(subcommand)]
108 command: CatalogCommand,
109 },
110 Subagent {
112 #[command(subcommand)]
114 command: CatalogCommand,
115 },
116 Mcp {
118 #[command(subcommand)]
120 command: CatalogCommand,
121 },
122 Tools {
124 #[command(subcommand)]
126 command: ToolsCommand,
127 },
128 Tui(TuiCommand),
130 Approval {
132 #[command(subcommand)]
134 command: ApprovalCommand,
135 },
136 Deferred {
138 #[command(subcommand)]
140 command: DeferredCommand,
141 },
142 Resume(ResumeCommand),
144 Reset(ResetCommand),
146 Diagnostics,
148 Rpc(RpcCommand),
150 ReplayCheck,
152 Update(UpdateCommand),
154 Config {
156 #[command(subcommand)]
158 command: ConfigCommand,
159 },
160 Completion {
162 shell: Shell,
164 },
165}
166
167#[derive(Clone, Debug, Args)]
169pub struct RunCommand {
170 #[arg(short = 'p', long = "prompt")]
172 pub prompt: Option<String>,
173 pub prompt_parts: Vec<String>,
175 #[arg(short = 's', conflicts_with_all = ["new_session", "continue_session"], long)]
177 pub session: Option<String>,
178 #[arg(long = "continue", conflicts_with = "new_session")]
180 pub continue_session: bool,
181 #[arg(long)]
183 pub new_session: bool,
184 #[arg(long)]
186 pub run: Option<String>,
187 #[arg(long, conflicts_with = "run")]
189 pub branch_from: Option<String>,
190 #[arg(long)]
192 pub profile: Option<String>,
193 #[arg(long, num_args = 0..=1, default_missing_value = "true")]
195 pub worker: Option<String>,
196 #[arg(long = "worker-label")]
198 pub worker_label: Option<String>,
199 #[arg(short = 'w', long, num_args = 0..=1, default_missing_value = "true")]
201 pub worktree: Option<String>,
202 #[arg(long = "worktree-name")]
204 pub worktree_name: Option<String>,
205 #[arg(long)]
207 pub branch: Option<String>,
208 #[arg(long)]
210 pub output: Option<OutputMode>,
211 #[arg(long)]
213 pub hitl: Option<HitlPolicy>,
214 #[arg(skip)]
216 pub goal: Option<GoalCommandOptions>,
217 #[arg(skip)]
219 pub session_affinity_id: Option<String>,
220 #[arg(skip)]
222 pub environment_attachments: Vec<starweaver_rpc_core::EnvironmentAttachmentRef>,
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct GoalCommandOptions {
228 pub objective: String,
230 pub max_iterations: usize,
232}
233
234impl RunCommand {
235 pub fn prompt_text(&self) -> CliResult<String> {
237 let prompt = self
238 .prompt
239 .clone()
240 .unwrap_or_else(|| self.prompt_parts.join(" "));
241 if prompt.trim().is_empty() {
242 Err(CliError::Usage(
243 "usage: starweaver-cli run -p <prompt>".to_string(),
244 ))
245 } else {
246 Ok(prompt)
247 }
248 }
249}
250
251#[derive(Clone, Debug, Subcommand)]
253pub enum SessionCommand {
254 List(SessionListCommand),
256 Show(SessionShowCommand),
258 Replay(SessionReplayCommand),
260 Delete(SessionDeleteCommand),
262 Trim(SessionTrimCommand),
264}
265
266#[derive(Clone, Debug, Args)]
268pub struct SessionListCommand {
269 #[arg(long, default_value = "display-jsonl")]
271 pub output: OutputMode,
272 #[arg(long, default_value_t = 50)]
274 pub limit: usize,
275}
276
277#[derive(Clone, Debug, Args)]
279pub struct SessionShowCommand {
280 pub session_id: String,
282 #[arg(long, default_value = "display-jsonl")]
284 pub output: OutputMode,
285 #[arg(long, default_value_t = 20)]
287 pub runs: usize,
288}
289
290#[derive(Clone, Debug, Args)]
292pub struct SessionReplayCommand {
293 pub session_id: String,
295 #[arg(long)]
297 pub run: Option<String>,
298 #[arg(long)]
300 pub after: Option<usize>,
301 #[arg(long, default_value = "display-jsonl")]
303 pub output: OutputMode,
304}
305
306#[derive(Clone, Debug, Args)]
308pub struct SessionDeleteCommand {
309 pub session_id: String,
311 #[arg(long)]
313 pub yes: bool,
314 #[arg(long, default_value = "text")]
316 pub output: OutputMode,
317}
318
319#[derive(Clone, Debug, Args)]
321pub struct SessionTrimCommand {
322 #[arg(long)]
324 pub current: bool,
325 #[arg(long)]
327 pub all: bool,
328 #[arg(long)]
330 pub session: Option<String>,
331 #[arg(long, default_value_t = 20)]
333 pub keep_runs: usize,
334 #[arg(long)]
336 pub older_than: Option<String>,
337 #[arg(long)]
339 pub dry_run: bool,
340 #[arg(long, default_value = "display-jsonl")]
342 pub output: OutputMode,
343}
344
345#[derive(Clone, Debug, Subcommand)]
347pub enum ProfileCommand {
348 List,
350 Show { name: String },
352}
353
354#[derive(Clone, Debug, Args)]
356pub struct SetupCommand {
357 #[arg(long, conflicts_with = "project")]
359 pub global: bool,
360 #[arg(long)]
362 pub project: bool,
363 #[arg(long)]
365 pub force: bool,
366}
367
368#[derive(Clone, Debug, Subcommand)]
370pub enum AuthCommand {
371 Login(AuthProviderCommand),
373 Status(AuthStatusCommand),
375 Refresh(AuthProviderCommand),
377 Logout(AuthLogoutCommand),
379 Doctor(AuthDoctorCommand),
381}
382
383#[derive(Clone, Debug, Args)]
385pub struct AuthProviderCommand {
386 #[arg(default_value = "codex", value_parser = ["codex"])]
388 pub provider: String,
389 #[arg(long = "auth-file")]
391 pub auth_file: Option<String>,
392 #[arg(long, default_value_t = 15 * 60)]
394 pub timeout_seconds: u64,
395}
396
397#[derive(Clone, Debug, Args)]
399pub struct AuthStatusCommand {
400 #[arg(default_value = "codex", value_parser = ["codex"])]
402 pub provider: Option<String>,
403 #[arg(long = "auth-file")]
405 pub auth_file: Option<String>,
406}
407
408#[derive(Clone, Debug, Args)]
410pub struct AuthLogoutCommand {
411 #[arg(default_value = "codex", value_parser = ["codex"])]
413 pub provider: String,
414 #[arg(long = "auth-file")]
416 pub auth_file: Option<String>,
417 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
419 pub revoke: bool,
420}
421
422#[derive(Clone, Debug, Args)]
424pub struct AuthDoctorCommand {
425 #[arg(long = "auth-file")]
427 pub auth_file: Option<String>,
428}
429
430#[derive(Clone, Debug, Subcommand)]
432pub enum CatalogCommand {
433 List,
435 Show { name: String },
437 Doctor,
439}
440
441#[derive(Clone, Debug, Subcommand)]
443pub enum ToolsCommand {
444 List,
446 Doctor,
448}
449
450#[derive(Clone, Debug, Args)]
452pub struct TuiCommand {
453 #[arg(long)]
455 pub session: Option<String>,
456 #[arg(long)]
458 pub run: Option<String>,
459 #[arg(long)]
461 pub after: Option<usize>,
462 #[arg(long)]
464 pub interactive: bool,
465 #[arg(long, conflicts_with = "interactive")]
467 pub snapshot: bool,
468 #[arg(long, default_value = "text")]
470 pub output: OutputMode,
471}
472
473#[derive(Clone, Debug, Subcommand)]
475pub enum ApprovalCommand {
476 List(ApprovalListCommand),
478 Show { approval_id: String },
480 Approve(ApprovalDecisionCommand),
482 Reject(ApprovalDecisionCommand),
484}
485
486#[derive(Clone, Debug, Args)]
488pub struct ApprovalListCommand {
489 #[arg(long)]
491 pub session: Option<String>,
492 #[arg(long)]
494 pub run: Option<String>,
495 #[arg(long, default_value = "display-jsonl")]
497 pub output: OutputMode,
498}
499
500#[derive(Clone, Debug, Args)]
502pub struct ApprovalDecisionCommand {
503 pub approval_id: String,
505 #[arg(long)]
507 pub reason: Option<String>,
508 #[arg(long, default_value = "text")]
510 pub output: OutputMode,
511}
512
513#[derive(Clone, Debug, Subcommand)]
515pub enum DeferredCommand {
516 List(DeferredListCommand),
518 Show { deferred_id: String },
520 Complete(DeferredCompleteCommand),
522 Fail(DeferredFailCommand),
524}
525
526#[derive(Clone, Debug, Args)]
528pub struct DeferredListCommand {
529 #[arg(long)]
531 pub session: Option<String>,
532 #[arg(long)]
534 pub run: Option<String>,
535 #[arg(long, default_value = "display-jsonl")]
537 pub output: OutputMode,
538}
539
540#[derive(Clone, Debug, Args)]
542pub struct DeferredCompleteCommand {
543 pub deferred_id: String,
545 #[arg(long)]
547 pub result: String,
548 #[arg(long, default_value = "text")]
550 pub output: OutputMode,
551}
552
553#[derive(Clone, Debug, Args)]
555pub struct DeferredFailCommand {
556 pub deferred_id: String,
558 #[arg(long)]
560 pub error: String,
561 #[arg(long, default_value = "text")]
563 pub output: OutputMode,
564}
565
566#[derive(Clone, Debug, Args)]
568pub struct ResumeCommand {
569 #[arg(long)]
571 pub session: Option<String>,
572 #[arg(long)]
574 pub run: Option<String>,
575 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
577 pub prompt: String,
578 #[arg(long)]
580 pub output: Option<OutputMode>,
581 #[arg(long)]
583 pub hitl: Option<HitlPolicy>,
584}
585
586#[derive(Clone, Debug, Args)]
588pub struct ResetCommand {
589 #[arg(long)]
591 pub yes: bool,
592 #[arg(long, default_value = "text")]
594 pub output: OutputMode,
595}
596
597#[derive(Clone, Debug, Args)]
599pub struct RpcCommand {
600 #[arg(default_value = "stdio")]
602 pub transport: RpcTransport,
603 #[arg(long, default_value = "127.0.0.1")]
605 pub host: String,
606 #[arg(long, default_value_t = 8765)]
608 pub port: u16,
609}
610
611#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
613#[serde(rename_all = "kebab-case")]
614pub enum RpcTransport {
615 #[default]
617 Stdio,
618 Http,
620}
621
622#[derive(Clone, Debug, Args)]
624pub struct UpdateCommand {
625 #[arg(default_value = "cli")]
627 pub target: String,
628 #[arg(long)]
630 pub dry_run: bool,
631 #[arg(long, short = 'f')]
633 pub force: bool,
634}
635
636#[derive(Clone, Debug, Subcommand)]
638pub enum ConfigCommand {
639 Init {
641 #[arg(long, conflicts_with = "project")]
643 global: bool,
644 #[arg(long)]
646 project: bool,
647 #[arg(long)]
649 force: bool,
650 },
651 Get { key: String },
653 Set {
655 #[arg(long, conflicts_with = "project")]
657 global: bool,
658 #[arg(long)]
660 project: bool,
661 key: String,
663 value: String,
665 },
666}
667
668#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
670#[serde(rename_all = "kebab-case")]
671pub enum OutputMode {
672 Text,
674 #[default]
676 DisplayJsonl,
677 AguiJsonl,
679 Json,
681 Silent,
683}
684
685#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
687#[serde(rename_all = "snake_case")]
688pub enum HitlPolicy {
689 Deny,
691 Defer,
693 Fail,
695 #[default]
697 Prompt,
698}
699
700#[must_use]
702pub fn command() -> clap::Command {
703 Cli::command()
704}
705
706pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
708 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
709}
710
711#[allow(dead_code)]
713pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
714 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
715}
716
717fn clap_error(error: &clap::Error) -> CliError {
718 match error.kind() {
719 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
720 CliError::Display(error.to_string())
721 }
722 _ => CliError::Usage(error.to_string()),
723 }
724}