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 ReplayCheck,
150 Update(UpdateCommand),
152 Config {
154 #[command(subcommand)]
156 command: ConfigCommand,
157 },
158 Completion {
160 shell: Shell,
162 },
163}
164
165#[derive(Clone, Debug, Args)]
167pub struct RunCommand {
168 #[arg(short = 'p', long = "prompt")]
170 pub prompt: Option<String>,
171 pub prompt_parts: Vec<String>,
173 #[arg(short = 's', conflicts_with_all = ["new_session", "continue_session"], long)]
175 pub session: Option<String>,
176 #[arg(long = "continue", conflicts_with = "new_session")]
178 pub continue_session: bool,
179 #[arg(long)]
181 pub new_session: bool,
182 #[arg(long)]
184 pub run: Option<String>,
185 #[arg(long, conflicts_with = "run")]
187 pub branch_from: Option<String>,
188 #[arg(long)]
190 pub profile: Option<String>,
191 #[arg(long, num_args = 0..=1, default_missing_value = "true")]
193 pub worker: Option<String>,
194 #[arg(long = "worker-label")]
196 pub worker_label: Option<String>,
197 #[arg(short = 'w', long, num_args = 0..=1, default_missing_value = "true")]
199 pub worktree: Option<String>,
200 #[arg(long = "worktree-name")]
202 pub worktree_name: Option<String>,
203 #[arg(long)]
205 pub branch: Option<String>,
206 #[arg(long)]
208 pub output: Option<OutputMode>,
209 #[arg(long)]
211 pub hitl: Option<HitlPolicy>,
212 #[arg(skip)]
214 pub goal: Option<GoalCommandOptions>,
215 #[arg(skip)]
217 pub session_affinity_id: Option<String>,
218 #[arg(skip)]
220 pub environment_attachments: Vec<starweaver_rpc_core::EnvironmentAttachmentRef>,
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
225pub struct GoalCommandOptions {
226 pub objective: String,
228 pub max_iterations: usize,
230}
231
232impl RunCommand {
233 pub fn prompt_text(&self) -> CliResult<String> {
235 let prompt = self
236 .prompt
237 .clone()
238 .unwrap_or_else(|| self.prompt_parts.join(" "));
239 if prompt.trim().is_empty() {
240 Err(CliError::Usage(
241 "usage: starweaver-cli run -p <prompt>".to_string(),
242 ))
243 } else {
244 Ok(prompt)
245 }
246 }
247}
248
249#[derive(Clone, Debug, Subcommand)]
251pub enum SessionCommand {
252 List(SessionListCommand),
254 Show(SessionShowCommand),
256 Replay(SessionReplayCommand),
258 Delete(SessionDeleteCommand),
260 Trim(SessionTrimCommand),
262}
263
264#[derive(Clone, Debug, Args)]
266pub struct SessionListCommand {
267 #[arg(long, default_value = "display-jsonl")]
269 pub output: OutputMode,
270 #[arg(long, default_value_t = 50)]
272 pub limit: usize,
273}
274
275#[derive(Clone, Debug, Args)]
277pub struct SessionShowCommand {
278 pub session_id: String,
280 #[arg(long, default_value = "display-jsonl")]
282 pub output: OutputMode,
283 #[arg(long, default_value_t = 20)]
285 pub runs: usize,
286}
287
288#[derive(Clone, Debug, Args)]
290pub struct SessionReplayCommand {
291 pub session_id: String,
293 #[arg(long)]
295 pub run: Option<String>,
296 #[arg(long)]
298 pub after: Option<usize>,
299 #[arg(long, default_value = "display-jsonl")]
301 pub output: OutputMode,
302}
303
304#[derive(Clone, Debug, Args)]
306pub struct SessionDeleteCommand {
307 pub session_id: String,
309 #[arg(long)]
311 pub yes: bool,
312 #[arg(long, default_value = "text")]
314 pub output: OutputMode,
315}
316
317#[derive(Clone, Debug, Args)]
319pub struct SessionTrimCommand {
320 #[arg(long)]
322 pub current: bool,
323 #[arg(long)]
325 pub all: bool,
326 #[arg(long)]
328 pub session: Option<String>,
329 #[arg(long, default_value_t = 20)]
331 pub keep_runs: usize,
332 #[arg(long)]
334 pub older_than: Option<String>,
335 #[arg(long)]
337 pub dry_run: bool,
338 #[arg(long, default_value = "display-jsonl")]
340 pub output: OutputMode,
341}
342
343#[derive(Clone, Debug, Subcommand)]
345pub enum ProfileCommand {
346 List,
348 Show { name: String },
350}
351
352#[derive(Clone, Debug, Args)]
354pub struct SetupCommand {
355 #[arg(long, conflicts_with = "project")]
357 pub global: bool,
358 #[arg(long)]
360 pub project: bool,
361 #[arg(long)]
363 pub force: bool,
364}
365
366#[derive(Clone, Debug, Subcommand)]
368pub enum AuthCommand {
369 Login(AuthProviderCommand),
371 Status(AuthStatusCommand),
373 Refresh(AuthProviderCommand),
375 Logout(AuthLogoutCommand),
377 Doctor(AuthDoctorCommand),
379}
380
381#[derive(Clone, Debug, Args)]
383pub struct AuthProviderCommand {
384 #[arg(default_value = "codex", value_parser = ["codex"])]
386 pub provider: String,
387 #[arg(long = "auth-file")]
389 pub auth_file: Option<String>,
390 #[arg(long, default_value_t = 15 * 60)]
392 pub timeout_seconds: u64,
393}
394
395#[derive(Clone, Debug, Args)]
397pub struct AuthStatusCommand {
398 #[arg(default_value = "codex", value_parser = ["codex"])]
400 pub provider: Option<String>,
401 #[arg(long = "auth-file")]
403 pub auth_file: Option<String>,
404}
405
406#[derive(Clone, Debug, Args)]
408pub struct AuthLogoutCommand {
409 #[arg(default_value = "codex", value_parser = ["codex"])]
411 pub provider: String,
412 #[arg(long = "auth-file")]
414 pub auth_file: Option<String>,
415 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
417 pub revoke: bool,
418}
419
420#[derive(Clone, Debug, Args)]
422pub struct AuthDoctorCommand {
423 #[arg(long = "auth-file")]
425 pub auth_file: Option<String>,
426}
427
428#[derive(Clone, Debug, Subcommand)]
430pub enum CatalogCommand {
431 List,
433 Show { name: String },
435 Doctor,
437}
438
439#[derive(Clone, Debug, Subcommand)]
441pub enum ToolsCommand {
442 List,
444 Doctor,
446}
447
448#[derive(Clone, Debug, Args)]
450pub struct TuiCommand {
451 #[arg(long)]
453 pub session: Option<String>,
454 #[arg(long)]
456 pub run: Option<String>,
457 #[arg(long)]
459 pub after: Option<usize>,
460 #[arg(long)]
462 pub interactive: bool,
463 #[arg(long, conflicts_with = "interactive")]
465 pub snapshot: bool,
466 #[arg(long, default_value = "text")]
468 pub output: OutputMode,
469 #[arg(long = "render-mode")]
471 pub render_mode: Option<TuiRenderMode>,
472}
473
474#[derive(Clone, Debug, Subcommand)]
476pub enum ApprovalCommand {
477 List(ApprovalListCommand),
479 Show { approval_id: String },
481 Approve(ApprovalDecisionCommand),
483 Reject(ApprovalDecisionCommand),
485}
486
487#[derive(Clone, Debug, Args)]
489pub struct ApprovalListCommand {
490 #[arg(long)]
492 pub session: Option<String>,
493 #[arg(long)]
495 pub run: Option<String>,
496 #[arg(long, default_value = "display-jsonl")]
498 pub output: OutputMode,
499}
500
501#[derive(Clone, Debug, Args)]
503pub struct ApprovalDecisionCommand {
504 pub approval_id: String,
506 #[arg(long)]
508 pub reason: Option<String>,
509 #[arg(long, default_value = "text")]
511 pub output: OutputMode,
512}
513
514#[derive(Clone, Debug, Subcommand)]
516pub enum DeferredCommand {
517 List(DeferredListCommand),
519 Show { deferred_id: String },
521 Complete(DeferredCompleteCommand),
523 Fail(DeferredFailCommand),
525}
526
527#[derive(Clone, Debug, Args)]
529pub struct DeferredListCommand {
530 #[arg(long)]
532 pub session: Option<String>,
533 #[arg(long)]
535 pub run: Option<String>,
536 #[arg(long, default_value = "display-jsonl")]
538 pub output: OutputMode,
539}
540
541#[derive(Clone, Debug, Args)]
543pub struct DeferredCompleteCommand {
544 pub deferred_id: String,
546 #[arg(long)]
548 pub result: String,
549 #[arg(long, default_value = "text")]
551 pub output: OutputMode,
552}
553
554#[derive(Clone, Debug, Args)]
556pub struct DeferredFailCommand {
557 pub deferred_id: String,
559 #[arg(long)]
561 pub error: String,
562 #[arg(long, default_value = "text")]
564 pub output: OutputMode,
565}
566
567#[derive(Clone, Debug, Args)]
569pub struct ResumeCommand {
570 #[arg(long)]
572 pub session: Option<String>,
573 #[arg(long)]
575 pub run: Option<String>,
576 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
578 pub prompt: String,
579 #[arg(long)]
581 pub output: Option<OutputMode>,
582 #[arg(long)]
584 pub hitl: Option<HitlPolicy>,
585}
586
587#[derive(Clone, Debug, Args)]
589pub struct ResetCommand {
590 #[arg(long)]
592 pub yes: bool,
593 #[arg(long, default_value = "text")]
595 pub output: OutputMode,
596}
597
598#[derive(Clone, Debug, Args)]
600pub struct UpdateCommand {
601 #[arg(default_value = "cli")]
603 pub target: String,
604 #[arg(long)]
606 pub dry_run: bool,
607 #[arg(long, short = 'f')]
609 pub force: bool,
610}
611
612#[derive(Clone, Debug, Subcommand)]
614pub enum ConfigCommand {
615 Init {
617 #[arg(long, conflicts_with = "project")]
619 global: bool,
620 #[arg(long)]
622 project: bool,
623 #[arg(long)]
625 force: bool,
626 },
627 Get { key: String },
629 Set {
631 #[arg(long, conflicts_with = "project")]
633 global: bool,
634 #[arg(long)]
636 project: bool,
637 key: String,
639 value: String,
641 },
642}
643
644#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
646#[serde(rename_all = "kebab-case")]
647pub enum TuiRenderMode {
648 #[default]
650 Normal,
651 Concise,
653 Debug,
655}
656
657#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
659#[serde(rename_all = "kebab-case")]
660pub enum OutputMode {
661 Text,
663 #[default]
665 DisplayJsonl,
666 AguiJsonl,
668 Json,
670 Silent,
672}
673
674#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
676#[serde(rename_all = "snake_case")]
677pub enum HitlPolicy {
678 Deny,
680 Defer,
682 Fail,
684 #[default]
686 Prompt,
687}
688
689#[must_use]
691pub fn command() -> clap::Command {
692 Cli::command()
693}
694
695pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
697 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
698}
699
700#[allow(dead_code)]
702pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
703 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
704}
705
706fn clap_error(error: &clap::Error) -> CliError {
707 match error.kind() {
708 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
709 CliError::Display(error.to_string())
710 }
711 _ => CliError::Usage(error.to_string()),
712 }
713}