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 session_affinity_id: Option<String>,
217}
218
219impl RunCommand {
220 pub fn prompt_text(&self) -> CliResult<String> {
222 let prompt = self
223 .prompt
224 .clone()
225 .unwrap_or_else(|| self.prompt_parts.join(" "));
226 if prompt.trim().is_empty() {
227 Err(CliError::Usage(
228 "usage: starweaver-cli run -p <prompt>".to_string(),
229 ))
230 } else {
231 Ok(prompt)
232 }
233 }
234}
235
236#[derive(Clone, Debug, Subcommand)]
238pub enum SessionCommand {
239 List(SessionListCommand),
241 Show(SessionShowCommand),
243 Replay(SessionReplayCommand),
245 Delete(SessionDeleteCommand),
247 Trim(SessionTrimCommand),
249}
250
251#[derive(Clone, Debug, Args)]
253pub struct SessionListCommand {
254 #[arg(long, default_value = "display-jsonl")]
256 pub output: OutputMode,
257 #[arg(long, default_value_t = 50)]
259 pub limit: usize,
260}
261
262#[derive(Clone, Debug, Args)]
264pub struct SessionShowCommand {
265 pub session_id: String,
267 #[arg(long, default_value = "display-jsonl")]
269 pub output: OutputMode,
270 #[arg(long, default_value_t = 20)]
272 pub runs: usize,
273}
274
275#[derive(Clone, Debug, Args)]
277pub struct SessionReplayCommand {
278 pub session_id: String,
280 #[arg(long)]
282 pub run: Option<String>,
283 #[arg(long)]
285 pub after: Option<usize>,
286 #[arg(long, default_value = "display-jsonl")]
288 pub output: OutputMode,
289}
290
291#[derive(Clone, Debug, Args)]
293pub struct SessionDeleteCommand {
294 pub session_id: String,
296 #[arg(long)]
298 pub yes: bool,
299 #[arg(long, default_value = "text")]
301 pub output: OutputMode,
302}
303
304#[derive(Clone, Debug, Args)]
306pub struct SessionTrimCommand {
307 #[arg(long)]
309 pub current: bool,
310 #[arg(long)]
312 pub all: bool,
313 #[arg(long)]
315 pub session: Option<String>,
316 #[arg(long, default_value_t = 20)]
318 pub keep_runs: usize,
319 #[arg(long)]
321 pub older_than: Option<String>,
322 #[arg(long)]
324 pub dry_run: bool,
325 #[arg(long, default_value = "display-jsonl")]
327 pub output: OutputMode,
328}
329
330#[derive(Clone, Debug, Subcommand)]
332pub enum ProfileCommand {
333 List,
335 Show { name: String },
337}
338
339#[derive(Clone, Debug, Args)]
341pub struct SetupCommand {
342 #[arg(long, conflicts_with = "project")]
344 pub global: bool,
345 #[arg(long)]
347 pub project: bool,
348 #[arg(long)]
350 pub force: bool,
351}
352
353#[derive(Clone, Debug, Subcommand)]
355pub enum AuthCommand {
356 Login(AuthProviderCommand),
358 Status(AuthStatusCommand),
360 Refresh(AuthProviderCommand),
362 Logout(AuthLogoutCommand),
364 Doctor(AuthDoctorCommand),
366}
367
368#[derive(Clone, Debug, Args)]
370pub struct AuthProviderCommand {
371 #[arg(default_value = "codex", value_parser = ["codex"])]
373 pub provider: String,
374 #[arg(long = "auth-file")]
376 pub auth_file: Option<String>,
377 #[arg(long, default_value_t = 15 * 60)]
379 pub timeout_seconds: u64,
380}
381
382#[derive(Clone, Debug, Args)]
384pub struct AuthStatusCommand {
385 #[arg(default_value = "codex", value_parser = ["codex"])]
387 pub provider: Option<String>,
388 #[arg(long = "auth-file")]
390 pub auth_file: Option<String>,
391}
392
393#[derive(Clone, Debug, Args)]
395pub struct AuthLogoutCommand {
396 #[arg(default_value = "codex", value_parser = ["codex"])]
398 pub provider: String,
399 #[arg(long = "auth-file")]
401 pub auth_file: Option<String>,
402 #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
404 pub revoke: bool,
405}
406
407#[derive(Clone, Debug, Args)]
409pub struct AuthDoctorCommand {
410 #[arg(long = "auth-file")]
412 pub auth_file: Option<String>,
413}
414
415#[derive(Clone, Debug, Subcommand)]
417pub enum CatalogCommand {
418 List,
420 Show { name: String },
422 Doctor,
424}
425
426#[derive(Clone, Debug, Subcommand)]
428pub enum ToolsCommand {
429 List,
431 Doctor,
433}
434
435#[derive(Clone, Debug, Args)]
437pub struct TuiCommand {
438 #[arg(long)]
440 pub session: Option<String>,
441 #[arg(long)]
443 pub run: Option<String>,
444 #[arg(long)]
446 pub after: Option<usize>,
447 #[arg(long)]
449 pub interactive: bool,
450 #[arg(long, conflicts_with = "interactive")]
452 pub snapshot: bool,
453 #[arg(long, default_value = "text")]
455 pub output: OutputMode,
456}
457
458#[derive(Clone, Debug, Subcommand)]
460pub enum ApprovalCommand {
461 List(ApprovalListCommand),
463 Show { approval_id: String },
465 Approve(ApprovalDecisionCommand),
467 Reject(ApprovalDecisionCommand),
469}
470
471#[derive(Clone, Debug, Args)]
473pub struct ApprovalListCommand {
474 #[arg(long)]
476 pub session: Option<String>,
477 #[arg(long)]
479 pub run: Option<String>,
480 #[arg(long, default_value = "display-jsonl")]
482 pub output: OutputMode,
483}
484
485#[derive(Clone, Debug, Args)]
487pub struct ApprovalDecisionCommand {
488 pub approval_id: String,
490 #[arg(long)]
492 pub reason: Option<String>,
493 #[arg(long, default_value = "text")]
495 pub output: OutputMode,
496}
497
498#[derive(Clone, Debug, Subcommand)]
500pub enum DeferredCommand {
501 List(DeferredListCommand),
503 Show { deferred_id: String },
505 Complete(DeferredCompleteCommand),
507 Fail(DeferredFailCommand),
509}
510
511#[derive(Clone, Debug, Args)]
513pub struct DeferredListCommand {
514 #[arg(long)]
516 pub session: Option<String>,
517 #[arg(long)]
519 pub run: Option<String>,
520 #[arg(long, default_value = "display-jsonl")]
522 pub output: OutputMode,
523}
524
525#[derive(Clone, Debug, Args)]
527pub struct DeferredCompleteCommand {
528 pub deferred_id: String,
530 #[arg(long)]
532 pub result: String,
533 #[arg(long, default_value = "text")]
535 pub output: OutputMode,
536}
537
538#[derive(Clone, Debug, Args)]
540pub struct DeferredFailCommand {
541 pub deferred_id: String,
543 #[arg(long)]
545 pub error: String,
546 #[arg(long, default_value = "text")]
548 pub output: OutputMode,
549}
550
551#[derive(Clone, Debug, Args)]
553pub struct ResumeCommand {
554 #[arg(long)]
556 pub session: Option<String>,
557 #[arg(long)]
559 pub run: Option<String>,
560 #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
562 pub prompt: String,
563 #[arg(long)]
565 pub output: Option<OutputMode>,
566 #[arg(long)]
568 pub hitl: Option<HitlPolicy>,
569}
570
571#[derive(Clone, Debug, Args)]
573pub struct ResetCommand {
574 #[arg(long)]
576 pub yes: bool,
577 #[arg(long, default_value = "text")]
579 pub output: OutputMode,
580}
581
582#[derive(Clone, Debug, Args)]
584pub struct RpcCommand {
585 #[arg(default_value = "stdio")]
587 pub transport: RpcTransport,
588}
589
590#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
592#[serde(rename_all = "kebab-case")]
593pub enum RpcTransport {
594 #[default]
596 Stdio,
597}
598
599#[derive(Clone, Debug, Args)]
601pub struct UpdateCommand {
602 #[arg(default_value = "cli")]
604 pub target: String,
605}
606
607#[derive(Clone, Debug, Subcommand)]
609pub enum ConfigCommand {
610 Init {
612 #[arg(long, conflicts_with = "project")]
614 global: bool,
615 #[arg(long)]
617 project: bool,
618 #[arg(long)]
620 force: bool,
621 },
622 Get { key: String },
624 Set {
626 #[arg(long, conflicts_with = "project")]
628 global: bool,
629 #[arg(long)]
631 project: bool,
632 key: String,
634 value: String,
636 },
637}
638
639#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
641#[serde(rename_all = "kebab-case")]
642pub enum OutputMode {
643 Text,
645 #[default]
647 DisplayJsonl,
648 AguiJsonl,
650 Json,
652 Silent,
654}
655
656#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
658#[serde(rename_all = "snake_case")]
659pub enum HitlPolicy {
660 Deny,
662 Defer,
664 Fail,
666 #[default]
668 Prompt,
669}
670
671#[must_use]
673pub fn command() -> clap::Command {
674 Cli::command()
675}
676
677pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
679 Cli::try_parse_from(args).map_err(|error| CliError::Usage(error.to_string()))
680}
681
682#[allow(dead_code)]
684pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
685 Cli::try_parse_from(args).map_err(|error| CliError::Usage(error.to_string()))
686}