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 Search(SessionSearchCommand),
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 SessionSearchCommand {
280 pub text: Option<String>,
282 #[arg(long)]
284 pub status: Option<SessionSearchStatusArg>,
285 #[arg(long)]
287 pub profile: Option<String>,
288 #[arg(long)]
290 pub workspace: Option<String>,
291 #[arg(long = "source", value_enum)]
293 pub sources: Vec<SessionSearchSourceArg>,
294 #[arg(long, value_enum, default_value = "session")]
296 pub granularity: SessionSearchGranularityArg,
297 #[arg(long, default_value_t = 20)]
299 pub limit: u32,
300 #[arg(long = "after")]
302 pub cursor: Option<String>,
303 #[arg(long, default_value = "text")]
305 pub output: OutputMode,
306}
307
308#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
310#[value(rename_all = "snake_case")]
311pub enum SessionSearchStatusArg {
312 Active,
314 Archived,
316 Failed,
318}
319
320#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
322#[value(rename_all = "snake_case")]
323pub enum SessionSearchSourceArg {
324 SessionMetadata,
326 RunInput,
328 RunOutputPreview,
330 #[value(alias = "display")]
332 DisplayMessage,
333}
334
335#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
337#[value(rename_all = "snake_case")]
338pub enum SessionSearchGranularityArg {
339 Session,
341 Run,
343 Occurrence,
345}
346
347#[derive(Clone, Debug, Args)]
349pub struct SessionShowCommand {
350 pub session_id: String,
352 #[arg(long, default_value = "display-jsonl")]
354 pub output: OutputMode,
355 #[arg(long, default_value_t = 20)]
357 pub runs: usize,
358}
359
360#[derive(Clone, Debug, Args)]
362pub struct SessionReplayCommand {
363 pub session_id: String,
365 #[arg(long)]
367 pub run: Option<String>,
368 #[arg(long)]
370 pub after: Option<usize>,
371 #[arg(long, default_value = "display-jsonl")]
373 pub output: OutputMode,
374}
375
376#[derive(Clone, Debug, Args)]
378pub struct SessionDeleteCommand {
379 pub session_id: String,
381 #[arg(long)]
383 pub yes: bool,
384 #[arg(long, default_value = "text")]
386 pub output: OutputMode,
387}
388
389#[derive(Clone, Debug, Args)]
391pub struct SessionTrimCommand {
392 #[arg(long)]
394 pub current: bool,
395 #[arg(long)]
397 pub all: bool,
398 #[arg(long)]
400 pub session: Option<String>,
401 #[arg(long, default_value_t = 20)]
403 pub keep_runs: usize,
404 #[arg(long)]
406 pub older_than: Option<String>,
407 #[arg(long)]
409 pub dry_run: bool,
410 #[arg(long, default_value = "display-jsonl")]
412 pub output: OutputMode,
413}
414
415#[derive(Clone, Debug, Subcommand)]
417pub enum ProfileCommand {
418 List,
420 Show { name: String },
422}
423
424#[derive(Clone, Debug, Args)]
426pub struct SetupCommand {
427 #[arg(long, conflicts_with = "project")]
429 pub global: bool,
430 #[arg(long)]
432 pub project: bool,
433 #[arg(long)]
435 pub force: bool,
436}
437
438#[derive(Clone, Debug, Subcommand)]
440pub enum AuthCommand {
441 Login(AuthProviderCommand),
443 Status(AuthStatusCommand),
445 Refresh(AuthProviderCommand),
447 Logout(AuthLogoutCommand),
449 Doctor(AuthDoctorCommand),
451}
452
453#[derive(Clone, Debug, Args)]
455pub struct AuthProviderCommand {
456 #[arg(default_value = "codex", value_parser = ["codex"])]
458 pub provider: String,
459 #[arg(long = "auth-file")]
461 pub auth_file: Option<String>,
462 #[arg(long, default_value_t = 15 * 60)]
464 pub timeout_seconds: u64,
465}
466
467#[derive(Clone, Debug, Args)]
469pub struct AuthStatusCommand {
470 #[arg(default_value = "codex", value_parser = ["codex"])]
472 pub provider: Option<String>,
473 #[arg(long = "auth-file")]
475 pub auth_file: Option<String>,
476}
477
478#[derive(Clone, Debug, Args)]
480pub struct AuthLogoutCommand {
481 #[arg(default_value = "codex", value_parser = ["codex"])]
483 pub provider: String,
484 #[arg(long = "auth-file")]
486 pub auth_file: Option<String>,
487 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
489 pub revoke: bool,
490}
491
492#[derive(Clone, Debug, Args)]
494pub struct AuthDoctorCommand {
495 #[arg(long = "auth-file")]
497 pub auth_file: Option<String>,
498}
499
500#[derive(Clone, Debug, Subcommand)]
502pub enum CatalogCommand {
503 List,
505 Show { name: String },
507 Doctor,
509}
510
511#[derive(Clone, Debug, Subcommand)]
513pub enum ToolsCommand {
514 List,
516 Doctor,
518}
519
520#[derive(Clone, Debug, Args)]
522pub struct TuiCommand {
523 #[arg(long)]
525 pub session: Option<String>,
526 #[arg(long)]
528 pub run: Option<String>,
529 #[arg(long)]
531 pub after: Option<usize>,
532 #[arg(long)]
534 pub interactive: bool,
535 #[arg(long, conflicts_with = "interactive")]
537 pub snapshot: bool,
538 #[arg(long, default_value = "text")]
540 pub output: OutputMode,
541 #[arg(long = "render-mode")]
543 pub render_mode: Option<TuiRenderMode>,
544}
545
546#[derive(Clone, Debug, Subcommand)]
548pub enum ApprovalCommand {
549 List(ApprovalListCommand),
551 Show { approval_id: String },
553 Approve(ApprovalDecisionCommand),
555 Reject(ApprovalDecisionCommand),
557}
558
559#[derive(Clone, Debug, Args)]
561pub struct ApprovalListCommand {
562 #[arg(long)]
564 pub session: Option<String>,
565 #[arg(long)]
567 pub run: Option<String>,
568 #[arg(long, default_value = "display-jsonl")]
570 pub output: OutputMode,
571}
572
573#[derive(Clone, Debug, Args)]
575pub struct ApprovalDecisionCommand {
576 pub approval_id: String,
578 #[arg(long)]
580 pub reason: Option<String>,
581 #[arg(long, default_value = "text")]
583 pub output: OutputMode,
584}
585
586#[derive(Clone, Debug, Subcommand)]
588pub enum DeferredCommand {
589 List(DeferredListCommand),
591 Show { deferred_id: String },
593 Complete(DeferredCompleteCommand),
595 Fail(DeferredFailCommand),
597}
598
599#[derive(Clone, Debug, Args)]
601pub struct DeferredListCommand {
602 #[arg(long)]
604 pub session: Option<String>,
605 #[arg(long)]
607 pub run: Option<String>,
608 #[arg(long, default_value = "display-jsonl")]
610 pub output: OutputMode,
611}
612
613#[derive(Clone, Debug, Args)]
615pub struct DeferredCompleteCommand {
616 pub deferred_id: String,
618 #[arg(long)]
620 pub result: String,
621 #[arg(long, default_value = "text")]
623 pub output: OutputMode,
624}
625
626#[derive(Clone, Debug, Args)]
628pub struct DeferredFailCommand {
629 pub deferred_id: String,
631 #[arg(long)]
633 pub error: String,
634 #[arg(long, default_value = "text")]
636 pub output: OutputMode,
637}
638
639#[derive(Clone, Debug, Args)]
641pub struct ResumeCommand {
642 #[arg(long)]
644 pub session: Option<String>,
645 #[arg(long)]
647 pub run: Option<String>,
648 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
650 pub prompt: String,
651 #[arg(long)]
653 pub output: Option<OutputMode>,
654 #[arg(long)]
656 pub hitl: Option<HitlPolicy>,
657}
658
659#[derive(Clone, Debug, Args)]
661pub struct ResetCommand {
662 #[arg(long)]
664 pub yes: bool,
665 #[arg(long, default_value = "text")]
667 pub output: OutputMode,
668}
669
670#[derive(Clone, Debug, Args)]
672pub struct UpdateCommand {
673 #[arg(default_value = "cli")]
675 pub target: String,
676 #[arg(long)]
678 pub dry_run: bool,
679 #[arg(long, short = 'f')]
681 pub force: bool,
682}
683
684#[derive(Clone, Debug, Subcommand)]
686pub enum ConfigCommand {
687 Init {
689 #[arg(long, conflicts_with = "project")]
691 global: bool,
692 #[arg(long)]
694 project: bool,
695 #[arg(long)]
697 force: bool,
698 },
699 Get { key: String },
701 Set {
703 #[arg(long, conflicts_with = "project")]
705 global: bool,
706 #[arg(long)]
708 project: bool,
709 key: String,
711 value: String,
713 },
714}
715
716#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
718#[serde(rename_all = "kebab-case")]
719pub enum TuiRenderMode {
720 #[default]
722 Normal,
723 Concise,
725 Debug,
727}
728
729#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
731#[serde(rename_all = "kebab-case")]
732pub enum OutputMode {
733 Text,
735 #[default]
737 DisplayJsonl,
738 AguiJsonl,
740 Json,
742 Silent,
744}
745
746#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
748#[serde(rename_all = "snake_case")]
749pub enum HitlPolicy {
750 Deny,
752 Defer,
754 Fail,
756 #[default]
758 Prompt,
759}
760
761#[must_use]
763pub fn command() -> clap::Command {
764 Cli::command()
765}
766
767pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
769 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
770}
771
772#[allow(dead_code)]
774pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
775 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
776}
777
778fn clap_error(error: &clap::Error) -> CliError {
779 match error.kind() {
780 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
781 CliError::Display(error.to_string())
782 }
783 _ => CliError::Usage(error.to_string()),
784 }
785}