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}
221
222#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct GoalCommandOptions {
225 pub objective: String,
227 pub max_iterations: usize,
229}
230
231impl RunCommand {
232 pub fn prompt_text(&self) -> CliResult<String> {
234 let prompt = self
235 .prompt
236 .clone()
237 .unwrap_or_else(|| self.prompt_parts.join(" "));
238 if prompt.trim().is_empty() {
239 Err(CliError::Usage(
240 "usage: starweaver-cli run -p <prompt>".to_string(),
241 ))
242 } else {
243 Ok(prompt)
244 }
245 }
246}
247
248#[derive(Clone, Debug, Subcommand)]
250pub enum SessionCommand {
251 List(SessionListCommand),
253 Show(SessionShowCommand),
255 Replay(SessionReplayCommand),
257 Delete(SessionDeleteCommand),
259 Trim(SessionTrimCommand),
261}
262
263#[derive(Clone, Debug, Args)]
265pub struct SessionListCommand {
266 #[arg(long, default_value = "display-jsonl")]
268 pub output: OutputMode,
269 #[arg(long, default_value_t = 50)]
271 pub limit: usize,
272}
273
274#[derive(Clone, Debug, Args)]
276pub struct SessionShowCommand {
277 pub session_id: String,
279 #[arg(long, default_value = "display-jsonl")]
281 pub output: OutputMode,
282 #[arg(long, default_value_t = 20)]
284 pub runs: usize,
285}
286
287#[derive(Clone, Debug, Args)]
289pub struct SessionReplayCommand {
290 pub session_id: String,
292 #[arg(long)]
294 pub run: Option<String>,
295 #[arg(long)]
297 pub after: Option<usize>,
298 #[arg(long, default_value = "display-jsonl")]
300 pub output: OutputMode,
301}
302
303#[derive(Clone, Debug, Args)]
305pub struct SessionDeleteCommand {
306 pub session_id: String,
308 #[arg(long)]
310 pub yes: bool,
311 #[arg(long, default_value = "text")]
313 pub output: OutputMode,
314}
315
316#[derive(Clone, Debug, Args)]
318pub struct SessionTrimCommand {
319 #[arg(long)]
321 pub current: bool,
322 #[arg(long)]
324 pub all: bool,
325 #[arg(long)]
327 pub session: Option<String>,
328 #[arg(long, default_value_t = 20)]
330 pub keep_runs: usize,
331 #[arg(long)]
333 pub older_than: Option<String>,
334 #[arg(long)]
336 pub dry_run: bool,
337 #[arg(long, default_value = "display-jsonl")]
339 pub output: OutputMode,
340}
341
342#[derive(Clone, Debug, Subcommand)]
344pub enum ProfileCommand {
345 List,
347 Show { name: String },
349}
350
351#[derive(Clone, Debug, Args)]
353pub struct SetupCommand {
354 #[arg(long, conflicts_with = "project")]
356 pub global: bool,
357 #[arg(long)]
359 pub project: bool,
360 #[arg(long)]
362 pub force: bool,
363}
364
365#[derive(Clone, Debug, Subcommand)]
367pub enum AuthCommand {
368 Login(AuthProviderCommand),
370 Status(AuthStatusCommand),
372 Refresh(AuthProviderCommand),
374 Logout(AuthLogoutCommand),
376 Doctor(AuthDoctorCommand),
378}
379
380#[derive(Clone, Debug, Args)]
382pub struct AuthProviderCommand {
383 #[arg(default_value = "codex", value_parser = ["codex"])]
385 pub provider: String,
386 #[arg(long = "auth-file")]
388 pub auth_file: Option<String>,
389 #[arg(long, default_value_t = 15 * 60)]
391 pub timeout_seconds: u64,
392}
393
394#[derive(Clone, Debug, Args)]
396pub struct AuthStatusCommand {
397 #[arg(default_value = "codex", value_parser = ["codex"])]
399 pub provider: Option<String>,
400 #[arg(long = "auth-file")]
402 pub auth_file: Option<String>,
403}
404
405#[derive(Clone, Debug, Args)]
407pub struct AuthLogoutCommand {
408 #[arg(default_value = "codex", value_parser = ["codex"])]
410 pub provider: String,
411 #[arg(long = "auth-file")]
413 pub auth_file: Option<String>,
414 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
416 pub revoke: bool,
417}
418
419#[derive(Clone, Debug, Args)]
421pub struct AuthDoctorCommand {
422 #[arg(long = "auth-file")]
424 pub auth_file: Option<String>,
425}
426
427#[derive(Clone, Debug, Subcommand)]
429pub enum CatalogCommand {
430 List,
432 Show { name: String },
434 Doctor,
436}
437
438#[derive(Clone, Debug, Subcommand)]
440pub enum ToolsCommand {
441 List,
443 Doctor,
445}
446
447#[derive(Clone, Debug, Args)]
449pub struct TuiCommand {
450 #[arg(long)]
452 pub session: Option<String>,
453 #[arg(long)]
455 pub run: Option<String>,
456 #[arg(long)]
458 pub after: Option<usize>,
459 #[arg(long)]
461 pub interactive: bool,
462 #[arg(long, conflicts_with = "interactive")]
464 pub snapshot: bool,
465 #[arg(long, default_value = "text")]
467 pub output: OutputMode,
468}
469
470#[derive(Clone, Debug, Subcommand)]
472pub enum ApprovalCommand {
473 List(ApprovalListCommand),
475 Show { approval_id: String },
477 Approve(ApprovalDecisionCommand),
479 Reject(ApprovalDecisionCommand),
481}
482
483#[derive(Clone, Debug, Args)]
485pub struct ApprovalListCommand {
486 #[arg(long)]
488 pub session: Option<String>,
489 #[arg(long)]
491 pub run: Option<String>,
492 #[arg(long, default_value = "display-jsonl")]
494 pub output: OutputMode,
495}
496
497#[derive(Clone, Debug, Args)]
499pub struct ApprovalDecisionCommand {
500 pub approval_id: String,
502 #[arg(long)]
504 pub reason: Option<String>,
505 #[arg(long, default_value = "text")]
507 pub output: OutputMode,
508}
509
510#[derive(Clone, Debug, Subcommand)]
512pub enum DeferredCommand {
513 List(DeferredListCommand),
515 Show { deferred_id: String },
517 Complete(DeferredCompleteCommand),
519 Fail(DeferredFailCommand),
521}
522
523#[derive(Clone, Debug, Args)]
525pub struct DeferredListCommand {
526 #[arg(long)]
528 pub session: Option<String>,
529 #[arg(long)]
531 pub run: Option<String>,
532 #[arg(long, default_value = "display-jsonl")]
534 pub output: OutputMode,
535}
536
537#[derive(Clone, Debug, Args)]
539pub struct DeferredCompleteCommand {
540 pub deferred_id: String,
542 #[arg(long)]
544 pub result: String,
545 #[arg(long, default_value = "text")]
547 pub output: OutputMode,
548}
549
550#[derive(Clone, Debug, Args)]
552pub struct DeferredFailCommand {
553 pub deferred_id: String,
555 #[arg(long)]
557 pub error: String,
558 #[arg(long, default_value = "text")]
560 pub output: OutputMode,
561}
562
563#[derive(Clone, Debug, Args)]
565pub struct ResumeCommand {
566 #[arg(long)]
568 pub session: Option<String>,
569 #[arg(long)]
571 pub run: Option<String>,
572 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
574 pub prompt: String,
575 #[arg(long)]
577 pub output: Option<OutputMode>,
578 #[arg(long)]
580 pub hitl: Option<HitlPolicy>,
581}
582
583#[derive(Clone, Debug, Args)]
585pub struct ResetCommand {
586 #[arg(long)]
588 pub yes: bool,
589 #[arg(long, default_value = "text")]
591 pub output: OutputMode,
592}
593
594#[derive(Clone, Debug, Args)]
596pub struct RpcCommand {
597 #[arg(default_value = "stdio")]
599 pub transport: RpcTransport,
600 #[arg(long, default_value = "127.0.0.1")]
602 pub host: String,
603 #[arg(long, default_value_t = 8765)]
605 pub port: u16,
606}
607
608#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
610#[serde(rename_all = "kebab-case")]
611pub enum RpcTransport {
612 #[default]
614 Stdio,
615 Http,
617}
618
619#[derive(Clone, Debug, Args)]
621pub struct UpdateCommand {
622 #[arg(default_value = "cli")]
624 pub target: String,
625 #[arg(long)]
627 pub dry_run: bool,
628 #[arg(long, short = 'f')]
630 pub force: bool,
631}
632
633#[derive(Clone, Debug, Subcommand)]
635pub enum ConfigCommand {
636 Init {
638 #[arg(long, conflicts_with = "project")]
640 global: bool,
641 #[arg(long)]
643 project: bool,
644 #[arg(long)]
646 force: bool,
647 },
648 Get { key: String },
650 Set {
652 #[arg(long, conflicts_with = "project")]
654 global: bool,
655 #[arg(long)]
657 project: bool,
658 key: String,
660 value: String,
662 },
663}
664
665#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
667#[serde(rename_all = "kebab-case")]
668pub enum OutputMode {
669 Text,
671 #[default]
673 DisplayJsonl,
674 AguiJsonl,
676 Json,
678 Silent,
680}
681
682#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
684#[serde(rename_all = "snake_case")]
685pub enum HitlPolicy {
686 Deny,
688 Defer,
690 Fail,
692 #[default]
694 Prompt,
695}
696
697#[must_use]
699pub fn command() -> clap::Command {
700 Cli::command()
701}
702
703pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
705 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
706}
707
708#[allow(dead_code)]
710pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
711 Cli::try_parse_from(args).map_err(|error| clap_error(&error))
712}
713
714fn clap_error(error: &clap::Error) -> CliError {
715 match error.kind() {
716 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
717 CliError::Display(error.to_string())
718 }
719 _ => CliError::Usage(error.to_string()),
720 }
721}