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 #[arg(long = "render-mode")]
473 pub render_mode: Option<TuiRenderMode>,
474}
475
476#[derive(Clone, Debug, Subcommand)]
478pub enum ApprovalCommand {
479 List(ApprovalListCommand),
481 Show { approval_id: String },
483 Approve(ApprovalDecisionCommand),
485 Reject(ApprovalDecisionCommand),
487}
488
489#[derive(Clone, Debug, Args)]
491pub struct ApprovalListCommand {
492 #[arg(long)]
494 pub session: Option<String>,
495 #[arg(long)]
497 pub run: Option<String>,
498 #[arg(long, default_value = "display-jsonl")]
500 pub output: OutputMode,
501}
502
503#[derive(Clone, Debug, Args)]
505pub struct ApprovalDecisionCommand {
506 pub approval_id: String,
508 #[arg(long)]
510 pub reason: Option<String>,
511 #[arg(long, default_value = "text")]
513 pub output: OutputMode,
514}
515
516#[derive(Clone, Debug, Subcommand)]
518pub enum DeferredCommand {
519 List(DeferredListCommand),
521 Show { deferred_id: String },
523 Complete(DeferredCompleteCommand),
525 Fail(DeferredFailCommand),
527}
528
529#[derive(Clone, Debug, Args)]
531pub struct DeferredListCommand {
532 #[arg(long)]
534 pub session: Option<String>,
535 #[arg(long)]
537 pub run: Option<String>,
538 #[arg(long, default_value = "display-jsonl")]
540 pub output: OutputMode,
541}
542
543#[derive(Clone, Debug, Args)]
545pub struct DeferredCompleteCommand {
546 pub deferred_id: String,
548 #[arg(long)]
550 pub result: String,
551 #[arg(long, default_value = "text")]
553 pub output: OutputMode,
554}
555
556#[derive(Clone, Debug, Args)]
558pub struct DeferredFailCommand {
559 pub deferred_id: String,
561 #[arg(long)]
563 pub error: String,
564 #[arg(long, default_value = "text")]
566 pub output: OutputMode,
567}
568
569#[derive(Clone, Debug, Args)]
571pub struct ResumeCommand {
572 #[arg(long)]
574 pub session: Option<String>,
575 #[arg(long)]
577 pub run: Option<String>,
578 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
580 pub prompt: String,
581 #[arg(long)]
583 pub output: Option<OutputMode>,
584 #[arg(long)]
586 pub hitl: Option<HitlPolicy>,
587}
588
589#[derive(Clone, Debug, Args)]
591pub struct ResetCommand {
592 #[arg(long)]
594 pub yes: bool,
595 #[arg(long, default_value = "text")]
597 pub output: OutputMode,
598}
599
600#[derive(Clone, Debug, Args)]
602pub struct RpcCommand {
603 #[arg(default_value = "stdio")]
605 pub transport: RpcTransport,
606 #[arg(long, default_value = "127.0.0.1")]
608 pub host: String,
609 #[arg(long, default_value_t = 8765)]
611 pub port: u16,
612}
613
614#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
616#[serde(rename_all = "kebab-case")]
617pub enum RpcTransport {
618 #[default]
620 Stdio,
621 Http,
623}
624
625#[derive(Clone, Debug, Args)]
627pub struct UpdateCommand {
628 #[arg(default_value = "cli")]
630 pub target: String,
631 #[arg(long)]
633 pub dry_run: bool,
634 #[arg(long, short = 'f')]
636 pub force: bool,
637}
638
639#[derive(Clone, Debug, Subcommand)]
641pub enum ConfigCommand {
642 Init {
644 #[arg(long, conflicts_with = "project")]
646 global: bool,
647 #[arg(long)]
649 project: bool,
650 #[arg(long)]
652 force: bool,
653 },
654 Get { key: String },
656 Set {
658 #[arg(long, conflicts_with = "project")]
660 global: bool,
661 #[arg(long)]
663 project: bool,
664 key: String,
666 value: String,
668 },
669}
670
671#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
673#[serde(rename_all = "kebab-case")]
674pub enum TuiRenderMode {
675 #[default]
677 Normal,
678 Concise,
680 Debug,
682}
683
684#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
686#[serde(rename_all = "kebab-case")]
687pub enum OutputMode {
688 Text,
690 #[default]
692 DisplayJsonl,
693 AguiJsonl,
695 Json,
697 Silent,
699}
700
701#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
703#[serde(rename_all = "snake_case")]
704pub enum HitlPolicy {
705 Deny,
707 Defer,
709 Fail,
711 #[default]
713 Prompt,
714}
715
716#[must_use]
718pub fn command() -> clap::Command {
719 Cli::command()
720}
721
722pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
724 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
725}
726
727#[allow(dead_code)]
729pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
730 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
731}
732
733fn clap_error(error: &clap::Error) -> CliError {
734 match error.kind() {
735 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
736 CliError::Display(error.to_string())
737 }
738 _ => CliError::Usage(error.to_string()),
739 }
740}