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 #[arg(skip)]
223 pub hitl_resume: bool,
224}
225
226#[derive(Clone, Debug, Eq, PartialEq)]
228pub struct GoalCommandOptions {
229 pub objective: String,
231 pub max_iterations: usize,
233}
234
235impl RunCommand {
236 pub fn prompt_text(&self) -> CliResult<String> {
238 let prompt = self
239 .prompt
240 .clone()
241 .unwrap_or_else(|| self.prompt_parts.join(" "));
242 if prompt.trim().is_empty() {
243 Err(CliError::Usage(
244 "usage: starweaver-cli run -p <prompt>".to_string(),
245 ))
246 } else {
247 Ok(prompt)
248 }
249 }
250}
251
252#[derive(Clone, Debug, Subcommand)]
254pub enum SessionCommand {
255 List(SessionListCommand),
257 Search(SessionSearchCommand),
259 Show(SessionShowCommand),
261 Replay(SessionReplayCommand),
263 Delete(SessionDeleteCommand),
265 Trim(SessionTrimCommand),
267}
268
269#[derive(Clone, Debug, Args)]
271pub struct SessionListCommand {
272 #[arg(long, default_value = "display-jsonl")]
274 pub output: OutputMode,
275 #[arg(long, default_value_t = 50)]
277 pub limit: usize,
278}
279
280#[derive(Clone, Debug, Args)]
282pub struct SessionSearchCommand {
283 pub text: Option<String>,
285 #[arg(long)]
287 pub status: Option<SessionSearchStatusArg>,
288 #[arg(long)]
290 pub profile: Option<String>,
291 #[arg(long)]
293 pub workspace: Option<String>,
294 #[arg(long = "source", value_enum)]
296 pub sources: Vec<SessionSearchSourceArg>,
297 #[arg(long, value_enum, default_value = "session")]
299 pub granularity: SessionSearchGranularityArg,
300 #[arg(long, default_value_t = 20)]
302 pub limit: u32,
303 #[arg(long = "after")]
305 pub cursor: Option<String>,
306 #[arg(long, default_value = "text")]
308 pub output: OutputMode,
309}
310
311#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
313#[value(rename_all = "snake_case")]
314pub enum SessionSearchStatusArg {
315 Active,
317 Archived,
319 Failed,
321}
322
323#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
325#[value(rename_all = "snake_case")]
326pub enum SessionSearchSourceArg {
327 SessionMetadata,
329 RunInput,
331 RunOutputPreview,
333 #[value(alias = "display")]
335 DisplayMessage,
336}
337
338#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
340#[value(rename_all = "snake_case")]
341pub enum SessionSearchGranularityArg {
342 Session,
344 Run,
346 Occurrence,
348}
349
350#[derive(Clone, Debug, Args)]
352pub struct SessionShowCommand {
353 pub session_id: String,
355 #[arg(long, default_value = "display-jsonl")]
357 pub output: OutputMode,
358 #[arg(long, default_value_t = 20)]
360 pub runs: usize,
361}
362
363#[derive(Clone, Debug, Args)]
365pub struct SessionReplayCommand {
366 pub session_id: String,
368 #[arg(long)]
370 pub run: Option<String>,
371 #[arg(long)]
373 pub after: Option<usize>,
374 #[arg(long, default_value = "display-jsonl")]
376 pub output: OutputMode,
377}
378
379#[derive(Clone, Debug, Args)]
381pub struct SessionDeleteCommand {
382 pub session_id: String,
384 #[arg(long)]
386 pub yes: bool,
387 #[arg(long, default_value = "text")]
389 pub output: OutputMode,
390}
391
392#[derive(Clone, Debug, Args)]
394pub struct SessionTrimCommand {
395 #[arg(long)]
397 pub current: bool,
398 #[arg(long)]
400 pub all: bool,
401 #[arg(long)]
403 pub session: Option<String>,
404 #[arg(long, default_value_t = 20)]
406 pub keep_runs: usize,
407 #[arg(long)]
409 pub older_than: Option<String>,
410 #[arg(long)]
412 pub dry_run: bool,
413 #[arg(long, default_value = "display-jsonl")]
415 pub output: OutputMode,
416}
417
418#[derive(Clone, Debug, Subcommand)]
420pub enum ProfileCommand {
421 List,
423 Show { name: String },
425}
426
427#[derive(Clone, Debug, Args)]
429pub struct SetupCommand {
430 #[arg(long, conflicts_with = "project")]
432 pub global: bool,
433 #[arg(long)]
435 pub project: bool,
436 #[arg(long)]
438 pub force: bool,
439}
440
441#[derive(Clone, Debug, Subcommand)]
443pub enum AuthCommand {
444 Login(AuthProviderCommand),
446 Status(AuthStatusCommand),
448 Refresh(AuthProviderCommand),
450 Logout(AuthLogoutCommand),
452 Doctor(AuthDoctorCommand),
454}
455
456#[derive(Clone, Debug, Args)]
458pub struct AuthProviderCommand {
459 #[arg(default_value = "codex", value_parser = ["codex"])]
461 pub provider: String,
462 #[arg(long = "auth-file")]
464 pub auth_file: Option<String>,
465 #[arg(long, default_value_t = 15 * 60)]
467 pub timeout_seconds: u64,
468}
469
470#[derive(Clone, Debug, Args)]
472pub struct AuthStatusCommand {
473 #[arg(default_value = "codex", value_parser = ["codex"])]
475 pub provider: Option<String>,
476 #[arg(long = "auth-file")]
478 pub auth_file: Option<String>,
479}
480
481#[derive(Clone, Debug, Args)]
483pub struct AuthLogoutCommand {
484 #[arg(default_value = "codex", value_parser = ["codex"])]
486 pub provider: String,
487 #[arg(long = "auth-file")]
489 pub auth_file: Option<String>,
490 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
492 pub revoke: bool,
493}
494
495#[derive(Clone, Debug, Args)]
497pub struct AuthDoctorCommand {
498 #[arg(long = "auth-file")]
500 pub auth_file: Option<String>,
501}
502
503#[derive(Clone, Debug, Subcommand)]
505pub enum CatalogCommand {
506 List,
508 Show { name: String },
510 Doctor,
512}
513
514#[derive(Clone, Debug, Subcommand)]
516pub enum ToolsCommand {
517 List,
519 Doctor,
521}
522
523#[derive(Clone, Debug, Args)]
525pub struct TuiCommand {
526 #[arg(long)]
528 pub session: Option<String>,
529 #[arg(long)]
531 pub run: Option<String>,
532 #[arg(long)]
534 pub after: Option<usize>,
535 #[arg(long)]
537 pub interactive: bool,
538 #[arg(long, conflicts_with = "interactive")]
540 pub snapshot: bool,
541 #[arg(long, default_value = "text")]
543 pub output: OutputMode,
544 #[arg(long = "render-mode")]
546 pub render_mode: Option<TuiRenderMode>,
547}
548
549#[derive(Clone, Debug, Subcommand)]
551pub enum ApprovalCommand {
552 List(ApprovalListCommand),
554 Show { approval_id: String },
556 Approve(ApprovalDecisionCommand),
558 Reject(ApprovalDecisionCommand),
560}
561
562#[derive(Clone, Debug, Args)]
564pub struct ApprovalListCommand {
565 #[arg(long)]
567 pub session: Option<String>,
568 #[arg(long)]
570 pub run: Option<String>,
571 #[arg(long, default_value = "display-jsonl")]
573 pub output: OutputMode,
574}
575
576#[derive(Clone, Debug, Args)]
578pub struct ApprovalDecisionCommand {
579 pub approval_id: String,
581 #[arg(long)]
583 pub reason: Option<String>,
584 #[arg(long, default_value = "text")]
586 pub output: OutputMode,
587}
588
589#[derive(Clone, Debug, Subcommand)]
591pub enum DeferredCommand {
592 List(DeferredListCommand),
594 Show { deferred_id: String },
596 Complete(DeferredCompleteCommand),
598 Fail(DeferredFailCommand),
600}
601
602#[derive(Clone, Debug, Args)]
604pub struct DeferredListCommand {
605 #[arg(long)]
607 pub session: Option<String>,
608 #[arg(long)]
610 pub run: Option<String>,
611 #[arg(long, default_value = "display-jsonl")]
613 pub output: OutputMode,
614}
615
616#[derive(Clone, Debug, Args)]
618pub struct DeferredCompleteCommand {
619 pub deferred_id: String,
621 #[arg(long)]
623 pub result: String,
624 #[arg(long, default_value = "text")]
626 pub output: OutputMode,
627}
628
629#[derive(Clone, Debug, Args)]
631pub struct DeferredFailCommand {
632 pub deferred_id: String,
634 #[arg(long)]
636 pub error: String,
637 #[arg(long, default_value = "text")]
639 pub output: OutputMode,
640}
641
642#[derive(Clone, Debug, Args)]
644pub struct ResumeCommand {
645 #[arg(long)]
647 pub session: Option<String>,
648 #[arg(long)]
650 pub run: Option<String>,
651 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
653 pub prompt: String,
654 #[arg(long)]
656 pub output: Option<OutputMode>,
657 #[arg(long)]
659 pub hitl: Option<HitlPolicy>,
660}
661
662#[derive(Clone, Debug, Args)]
664pub struct ResetCommand {
665 #[arg(long)]
667 pub yes: bool,
668 #[arg(long, default_value = "text")]
670 pub output: OutputMode,
671}
672
673#[derive(Clone, Debug, Args)]
675pub struct UpdateCommand {
676 #[arg(default_value = "cli")]
678 pub target: String,
679 #[arg(long)]
681 pub dry_run: bool,
682 #[arg(long, short = 'f')]
684 pub force: bool,
685}
686
687#[derive(Clone, Debug, Subcommand)]
689pub enum ConfigCommand {
690 Init {
692 #[arg(long, conflicts_with = "project")]
694 global: bool,
695 #[arg(long)]
697 project: bool,
698 #[arg(long)]
700 force: bool,
701 },
702 Get { key: String },
704 Set {
706 #[arg(long, conflicts_with = "project")]
708 global: bool,
709 #[arg(long)]
711 project: bool,
712 key: String,
714 value: String,
716 },
717}
718
719#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
721#[serde(rename_all = "kebab-case")]
722pub enum TuiRenderMode {
723 #[default]
725 Normal,
726 Concise,
728 Debug,
730}
731
732#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
734#[serde(rename_all = "kebab-case")]
735pub enum OutputMode {
736 Text,
738 #[default]
740 DisplayJsonl,
741 AguiJsonl,
743 Json,
745 Silent,
747}
748
749#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
751#[serde(rename_all = "snake_case")]
752pub enum HitlPolicy {
753 Deny,
755 Defer,
757 Fail,
759 #[default]
761 Prompt,
762}
763
764#[must_use]
766pub fn command() -> clap::Command {
767 Cli::command()
768}
769
770pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
772 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
773}
774
775#[allow(dead_code)]
777pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
778 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
779}
780
781fn clap_error(error: &clap::Error) -> CliError {
782 match error.kind() {
783 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
784 CliError::Display(error.to_string())
785 }
786 _ => CliError::Usage(error.to_string()),
787 }
788}