1use std::{ffi::OsString, path::PathBuf};
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, default_value = "preserve")]
43 pub continuation_mode: ContinuationModeArg,
44 #[arg(long, global = false, num_args = 0..=1, default_missing_value = "true")]
46 pub worker: Option<String>,
47 #[arg(long = "worker-label", global = false)]
49 pub worker_label: Option<String>,
50 #[arg(
52 short = 'w',
53 long,
54 global = false,
55 num_args = 0..=1,
56 default_missing_value = "true"
57 )]
58 pub worktree: Option<String>,
59 #[arg(long = "worktree-name", global = false)]
61 pub worktree_name: Option<String>,
62 #[arg(long, global = false)]
64 pub branch: Option<String>,
65 #[arg(long, global = false)]
67 pub output: Option<OutputMode>,
68 #[arg(long, global = false)]
70 pub hitl: Option<HitlPolicy>,
71 #[arg(long, global = true)]
73 pub store: Option<String>,
74 #[command(subcommand)]
76 pub command: Option<CliCommand>,
77}
78
79#[allow(clippy::large_enum_variant)]
81#[derive(Clone, Debug, Subcommand)]
82pub enum CliCommand {
83 Version,
85 Run(RunCommand),
87 Session {
89 #[command(subcommand)]
91 command: SessionCommand,
92 },
93 Storage {
95 #[command(subcommand)]
97 command: StorageCommand,
98 },
99 Profile {
101 #[command(subcommand)]
103 command: ProfileCommand,
104 },
105 Setup(SetupCommand),
107 Auth {
109 #[command(subcommand)]
111 command: AuthCommand,
112 },
113 Skill {
115 #[command(subcommand)]
117 command: CatalogCommand,
118 },
119 Subagent {
121 #[command(subcommand)]
123 command: CatalogCommand,
124 },
125 Mcp {
127 #[command(subcommand)]
129 command: CatalogCommand,
130 },
131 Tools {
133 #[command(subcommand)]
135 command: ToolsCommand,
136 },
137 Tui(TuiCommand),
139 Approval {
141 #[command(subcommand)]
143 command: ApprovalCommand,
144 },
145 Deferred {
147 #[command(subcommand)]
149 command: DeferredCommand,
150 },
151 Resume(ResumeCommand),
153 Reset(ResetCommand),
155 Diagnostics,
157 ReplayCheck,
159 Update(UpdateCommand),
161 Config {
163 #[command(subcommand)]
165 command: ConfigCommand,
166 },
167 Completion {
169 shell: Shell,
171 },
172}
173
174#[derive(Clone, Debug, Args)]
176pub struct RunCommand {
177 #[arg(short = 'p', long = "prompt")]
179 pub prompt: Option<String>,
180 pub prompt_parts: Vec<String>,
182 #[arg(short = 's', conflicts_with_all = ["new_session", "continue_session"], long)]
184 pub session: Option<String>,
185 #[arg(long = "continue", conflicts_with = "new_session")]
187 pub continue_session: bool,
188 #[arg(long)]
190 pub new_session: bool,
191 #[arg(long)]
193 pub run: Option<String>,
194 #[arg(long, conflicts_with = "run")]
196 pub branch_from: Option<String>,
197 #[arg(long)]
199 pub profile: Option<String>,
200 #[arg(long, default_value = "preserve")]
202 pub continuation_mode: ContinuationModeArg,
203 #[arg(long, num_args = 0..=1, default_missing_value = "true")]
205 pub worker: Option<String>,
206 #[arg(long = "worker-label")]
208 pub worker_label: Option<String>,
209 #[arg(short = 'w', long, num_args = 0..=1, default_missing_value = "true")]
211 pub worktree: Option<String>,
212 #[arg(long = "worktree-name")]
214 pub worktree_name: Option<String>,
215 #[arg(long)]
217 pub branch: Option<String>,
218 #[arg(long)]
220 pub output: Option<OutputMode>,
221 #[arg(long)]
223 pub hitl: Option<HitlPolicy>,
224 #[arg(skip)]
226 pub goal: Option<GoalCommandOptions>,
227 #[arg(skip)]
229 pub session_affinity_id: Option<String>,
230 #[arg(skip)]
232 pub(crate) environment_attachments: Vec<crate::environment::EnvironmentAttachmentRef>,
233 #[arg(skip)]
235 pub hitl_resume: bool,
236}
237
238#[derive(Clone, Debug, Eq, PartialEq)]
240pub struct GoalCommandOptions {
241 pub objective: String,
243 pub max_iterations: usize,
245}
246
247impl RunCommand {
248 pub fn prompt_text(&self) -> CliResult<String> {
250 let prompt = self
251 .prompt
252 .clone()
253 .unwrap_or_else(|| self.prompt_parts.join(" "));
254 if prompt.trim().is_empty() {
255 Err(CliError::Usage(
256 "usage: starweaver-cli run -p <prompt>".to_string(),
257 ))
258 } else {
259 Ok(prompt)
260 }
261 }
262}
263
264#[derive(Clone, Debug, Subcommand)]
266pub enum StorageCommand {
267 ImportLegacy(StorageImportLegacyCommand),
269}
270
271#[derive(Clone, Debug, Args)]
273pub struct StorageImportLegacyCommand {
274 #[arg(long)]
276 pub source: Option<PathBuf>,
277 #[arg(long)]
279 pub workspace: Option<PathBuf>,
280 #[arg(long, default_value = "text")]
282 pub output: OutputMode,
283}
284
285#[derive(Clone, Debug, Subcommand)]
287pub enum SessionCommand {
288 List(SessionListCommand),
290 Search(SessionSearchCommand),
292 Show(SessionShowCommand),
294 Replay(SessionReplayCommand),
296 Delete(SessionDeleteCommand),
298 Trim(SessionTrimCommand),
300}
301
302#[derive(Clone, Debug, Args)]
304pub struct SessionListCommand {
305 #[arg(long, default_value = "display-jsonl")]
307 pub output: OutputMode,
308 #[arg(long, default_value_t = 50)]
310 pub limit: usize,
311}
312
313#[derive(Clone, Debug, Args)]
315pub struct SessionSearchCommand {
316 pub text: Option<String>,
318 #[arg(long)]
320 pub status: Option<SessionSearchStatusArg>,
321 #[arg(long)]
323 pub profile: Option<String>,
324 #[arg(long)]
326 pub workspace: Option<String>,
327 #[arg(long = "source", value_enum)]
329 pub sources: Vec<SessionSearchSourceArg>,
330 #[arg(long, value_enum, default_value = "session")]
332 pub granularity: SessionSearchGranularityArg,
333 #[arg(long, default_value_t = 20)]
335 pub limit: u32,
336 #[arg(long = "after")]
338 pub cursor: Option<String>,
339 #[arg(long, default_value = "text")]
341 pub output: OutputMode,
342}
343
344#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
346#[value(rename_all = "snake_case")]
347pub enum SessionSearchStatusArg {
348 Active,
350 Archived,
352 Failed,
354}
355
356#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
358#[value(rename_all = "snake_case")]
359pub enum SessionSearchSourceArg {
360 SessionMetadata,
362 RunInput,
364 RunOutputPreview,
366 #[value(alias = "display")]
368 DisplayMessage,
369}
370
371#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
373#[value(rename_all = "snake_case")]
374pub enum SessionSearchGranularityArg {
375 Session,
377 Run,
379 Occurrence,
381}
382
383#[derive(Clone, Debug, Args)]
385pub struct SessionShowCommand {
386 pub session_id: String,
388 #[arg(long, default_value = "display-jsonl")]
390 pub output: OutputMode,
391 #[arg(long, default_value_t = 20)]
393 pub runs: usize,
394}
395
396#[derive(Clone, Debug, Args)]
398pub struct SessionReplayCommand {
399 pub session_id: String,
401 #[arg(long)]
403 pub run: Option<String>,
404 #[arg(long)]
406 pub after: Option<usize>,
407 #[arg(long, default_value = "display-jsonl")]
409 pub output: OutputMode,
410}
411
412#[derive(Clone, Debug, Args)]
414pub struct SessionDeleteCommand {
415 pub session_id: String,
417 #[arg(long)]
419 pub yes: bool,
420 #[arg(long, default_value = "text")]
422 pub output: OutputMode,
423}
424
425#[derive(Clone, Debug, Args)]
427pub struct SessionTrimCommand {
428 #[arg(long)]
430 pub current: bool,
431 #[arg(long)]
433 pub all: bool,
434 #[arg(long)]
436 pub session: Option<String>,
437 #[arg(long, default_value_t = 20)]
439 pub keep_runs: usize,
440 #[arg(long)]
442 pub older_than: Option<String>,
443 #[arg(long)]
445 pub dry_run: bool,
446 #[arg(long, default_value = "display-jsonl")]
448 pub output: OutputMode,
449}
450
451#[derive(Clone, Debug, Subcommand)]
453pub enum ProfileCommand {
454 List,
456 Show { name: String },
458}
459
460#[derive(Clone, Debug, Args)]
462pub struct SetupCommand {
463 #[arg(long, conflicts_with = "project")]
465 pub global: bool,
466 #[arg(long)]
468 pub project: bool,
469 #[arg(long)]
471 pub force: bool,
472}
473
474#[derive(Clone, Debug, Subcommand)]
476pub enum AuthCommand {
477 Login(AuthProviderCommand),
479 Status(AuthStatusCommand),
481 Refresh(AuthProviderCommand),
483 Logout(AuthLogoutCommand),
485 Doctor(AuthDoctorCommand),
487}
488
489#[derive(Clone, Debug, Args)]
491pub struct AuthProviderCommand {
492 #[arg(default_value = "codex", value_parser = ["codex"])]
494 pub provider: String,
495 #[arg(long = "auth-file")]
497 pub auth_file: Option<String>,
498 #[arg(long, default_value_t = 15 * 60)]
500 pub timeout_seconds: u64,
501}
502
503#[derive(Clone, Debug, Args)]
505pub struct AuthStatusCommand {
506 #[arg(default_value = "codex", value_parser = ["codex"])]
508 pub provider: Option<String>,
509 #[arg(long = "auth-file")]
511 pub auth_file: Option<String>,
512}
513
514#[derive(Clone, Debug, Args)]
516pub struct AuthLogoutCommand {
517 #[arg(default_value = "codex", value_parser = ["codex"])]
519 pub provider: String,
520 #[arg(long = "auth-file")]
522 pub auth_file: Option<String>,
523 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
525 pub revoke: bool,
526}
527
528#[derive(Clone, Debug, Args)]
530pub struct AuthDoctorCommand {
531 #[arg(long = "auth-file")]
533 pub auth_file: Option<String>,
534}
535
536#[derive(Clone, Debug, Subcommand)]
538pub enum CatalogCommand {
539 List,
541 Show { name: String },
543 Doctor,
545}
546
547#[derive(Clone, Debug, Subcommand)]
549pub enum ToolsCommand {
550 List,
552 Doctor,
554}
555
556#[derive(Clone, Debug, Args)]
558pub struct TuiCommand {
559 #[arg(long)]
561 pub session: Option<String>,
562 #[arg(long)]
564 pub run: Option<String>,
565 #[arg(long)]
567 pub after: Option<usize>,
568 #[arg(long)]
570 pub interactive: bool,
571 #[arg(long, conflicts_with = "interactive")]
573 pub snapshot: bool,
574 #[arg(long, default_value = "text")]
576 pub output: OutputMode,
577 #[arg(long = "render-mode")]
579 pub render_mode: Option<TuiRenderMode>,
580}
581
582#[derive(Clone, Debug, Subcommand)]
584pub enum ApprovalCommand {
585 List(ApprovalListCommand),
587 Show { approval_id: String },
589 Approve(ApprovalDecisionCommand),
591 Reject(ApprovalDecisionCommand),
593}
594
595#[derive(Clone, Debug, Args)]
597pub struct ApprovalListCommand {
598 #[arg(long)]
600 pub session: Option<String>,
601 #[arg(long)]
603 pub run: Option<String>,
604 #[arg(long, default_value = "display-jsonl")]
606 pub output: OutputMode,
607}
608
609#[derive(Clone, Debug, Args)]
611pub struct ApprovalDecisionCommand {
612 pub approval_id: String,
614 #[arg(long)]
616 pub reason: Option<String>,
617 #[arg(long, default_value = "text")]
619 pub output: OutputMode,
620}
621
622#[derive(Clone, Debug, Subcommand)]
624pub enum DeferredCommand {
625 List(DeferredListCommand),
627 Show { deferred_id: String },
629 Complete(DeferredCompleteCommand),
631 Fail(DeferredFailCommand),
633}
634
635#[derive(Clone, Debug, Args)]
637pub struct DeferredListCommand {
638 #[arg(long)]
640 pub session: Option<String>,
641 #[arg(long)]
643 pub run: Option<String>,
644 #[arg(long, default_value = "display-jsonl")]
646 pub output: OutputMode,
647}
648
649#[derive(Clone, Debug, Args)]
651pub struct DeferredCompleteCommand {
652 pub deferred_id: String,
654 #[arg(long)]
656 pub result: String,
657 #[arg(long, default_value = "text")]
659 pub output: OutputMode,
660}
661
662#[derive(Clone, Debug, Args)]
664pub struct DeferredFailCommand {
665 pub deferred_id: String,
667 #[arg(long)]
669 pub error: String,
670 #[arg(long, default_value = "text")]
672 pub output: OutputMode,
673}
674
675#[derive(Clone, Debug, Args)]
677pub struct ResumeCommand {
678 #[arg(long)]
680 pub session: Option<String>,
681 #[arg(long)]
683 pub run: Option<String>,
684 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
686 pub prompt: String,
687 #[arg(long)]
689 pub output: Option<OutputMode>,
690 #[arg(long)]
692 pub hitl: Option<HitlPolicy>,
693 #[arg(long, default_value = "preserve")]
695 pub continuation_mode: ContinuationModeArg,
696}
697
698#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
700#[serde(rename_all = "snake_case")]
701pub enum ContinuationModeArg {
702 #[default]
704 Preserve,
705 Compatible,
707 Switch,
709}
710
711impl From<ContinuationModeArg> for starweaver_agent::ContinuationMaterializationMode {
712 fn from(value: ContinuationModeArg) -> Self {
713 match value {
714 ContinuationModeArg::Preserve => Self::Preserve,
715 ContinuationModeArg::Compatible => Self::Compatible,
716 ContinuationModeArg::Switch => Self::Switch,
717 }
718 }
719}
720
721#[derive(Clone, Debug, Args)]
723pub struct ResetCommand {
724 #[arg(long)]
726 pub yes: bool,
727 #[arg(long, default_value = "text")]
729 pub output: OutputMode,
730}
731
732#[derive(Clone, Debug, Args)]
734pub struct UpdateCommand {
735 #[arg(default_value = "cli")]
737 pub target: String,
738 #[arg(long)]
740 pub dry_run: bool,
741 #[arg(long, short = 'f')]
743 pub force: bool,
744}
745
746#[derive(Clone, Debug, Subcommand)]
748pub enum ConfigCommand {
749 Init {
751 #[arg(long, conflicts_with = "project")]
753 global: bool,
754 #[arg(long)]
756 project: bool,
757 #[arg(long)]
759 force: bool,
760 },
761 Get { key: String },
763 Set {
765 #[arg(long, conflicts_with = "project")]
767 global: bool,
768 #[arg(long)]
770 project: bool,
771 key: String,
773 value: String,
775 },
776}
777
778#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
780#[serde(rename_all = "kebab-case")]
781pub enum TuiRenderMode {
782 #[default]
784 Normal,
785 Concise,
787 Debug,
789}
790
791#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
793#[serde(rename_all = "kebab-case")]
794pub enum OutputMode {
795 Text,
797 #[default]
799 DisplayJsonl,
800 AguiJsonl,
802 Json,
804 Silent,
806}
807
808#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
810#[serde(rename_all = "snake_case")]
811pub enum HitlPolicy {
812 Deny,
814 Defer,
816 Fail,
818 #[default]
820 Prompt,
821}
822
823#[must_use]
825pub fn command() -> clap::Command {
826 Cli::command()
827}
828
829pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
831 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
832}
833
834#[allow(dead_code)]
836pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
837 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
838}
839
840fn clap_error(error: &clap::Error) -> CliError {
841 match error.kind() {
842 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
843 CliError::Display(error.to_string())
844 }
845 _ => CliError::Usage(error.to_string()),
846 }
847}