Skip to main content

starweaver_cli/
args.rs

1//! CLI argument parsing.
2
3use 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/// Top-level CLI.
12#[derive(Clone, Debug, Parser)]
13#[command(name = "starweaver-cli", version, about = "Starweaver local CLI")]
14pub struct Cli {
15    /// Prompt shorthand for headless runs.
16    #[arg(short = 'p', long = "prompt", global = false)]
17    pub prompt: Option<String>,
18    /// Append a run to the selected session.
19    #[arg(
20        short = 's',
21        long,
22        global = false,
23        conflicts_with_all = ["new_session", "continue_session"]
24    )]
25    pub session: Option<String>,
26    /// Continue the latest local session.
27    #[arg(long = "continue", global = false, conflicts_with = "new_session")]
28    pub continue_session: bool,
29    /// Create a fresh session.
30    #[arg(long, global = false)]
31    pub new_session: bool,
32    /// Restore from a specific run before appending a run.
33    #[arg(long, global = false)]
34    pub run: Option<String>,
35    /// Branch from a specific run before appending a run.
36    #[arg(long, global = false, conflicts_with = "run")]
37    pub branch_from: Option<String>,
38    /// Agent profile name or YAML path.
39    #[arg(long, global = false)]
40    pub profile: Option<String>,
41    /// Enable worker mode or set an optional worker label.
42    #[arg(long, global = false, num_args = 0..=1, default_missing_value = "true")]
43    pub worker: Option<String>,
44    /// Explicit worker label.
45    #[arg(long = "worker-label", global = false)]
46    pub worker_label: Option<String>,
47    /// Enable a git worktree or set an optional worktree name/path.
48    #[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    /// Explicit worktree name/path.
57    #[arg(long = "worktree-name", global = false)]
58    pub worktree_name: Option<String>,
59    /// Git branch for worktree metadata.
60    #[arg(long, global = false)]
61    pub branch: Option<String>,
62    /// Output mode.
63    #[arg(long, global = false)]
64    pub output: Option<OutputMode>,
65    /// Headless human-in-the-loop policy for prompt shorthand.
66    #[arg(long, global = false)]
67    pub hitl: Option<HitlPolicy>,
68    /// Override local store database path.
69    #[arg(long, global = true)]
70    pub store: Option<String>,
71    /// Optional subcommand.
72    #[command(subcommand)]
73    pub command: Option<CliCommand>,
74}
75
76/// CLI command families.
77#[allow(clippy::large_enum_variant)]
78#[derive(Clone, Debug, Subcommand)]
79pub enum CliCommand {
80    /// Print SDK identity.
81    Version,
82    /// Run a prompt.
83    Run(RunCommand),
84    /// Manage local sessions.
85    Session {
86        /// Session subcommand.
87        #[command(subcommand)]
88        command: SessionCommand,
89    },
90    /// Manage agent profiles.
91    Profile {
92        /// Profile subcommand.
93        #[command(subcommand)]
94        command: ProfileCommand,
95    },
96    /// Initialize local CLI configuration and catalogs.
97    Setup(SetupCommand),
98    /// Inspect OAuth-backed provider authentication.
99    Auth {
100        /// Auth subcommand.
101        #[command(subcommand)]
102        command: AuthCommand,
103    },
104    /// Inspect configured skills.
105    Skill {
106        /// Skill subcommand.
107        #[command(subcommand)]
108        command: CatalogCommand,
109    },
110    /// Inspect configured subagents.
111    Subagent {
112        /// Subagent subcommand.
113        #[command(subcommand)]
114        command: CatalogCommand,
115    },
116    /// Inspect configured MCP servers.
117    Mcp {
118        /// MCP subcommand.
119        #[command(subcommand)]
120        command: CatalogCommand,
121    },
122    /// Inspect default CLI tool catalog and policy.
123    Tools {
124        /// Tools subcommand.
125        #[command(subcommand)]
126        command: ToolsCommand,
127    },
128    /// Render a retained terminal UI from local session display messages.
129    Tui(TuiCommand),
130    /// Manage persisted approval requests.
131    Approval {
132        /// Approval subcommand.
133        #[command(subcommand)]
134        command: ApprovalCommand,
135    },
136    /// Manage deferred tool calls.
137    Deferred {
138        /// Deferred subcommand.
139        #[command(subcommand)]
140        command: DeferredCommand,
141    },
142    /// Resume a waiting session by appending a continuation run.
143    Resume(ResumeCommand),
144    /// Remove runtime session state while preserving configuration.
145    Reset(ResetCommand),
146    /// Print diagnostics.
147    Diagnostics,
148    /// Run the JSON-RPC host service.
149    Rpc(RpcCommand),
150    /// Print replay-check guidance.
151    ReplayCheck,
152    /// Update installed Starweaver components.
153    Update(UpdateCommand),
154    /// Get or set configuration values.
155    Config {
156        /// Config subcommand.
157        #[command(subcommand)]
158        command: ConfigCommand,
159    },
160    /// Generate shell completion scripts.
161    Completion {
162        /// Target shell.
163        shell: Shell,
164    },
165}
166
167/// Prompt run command.
168#[derive(Clone, Debug, Args)]
169pub struct RunCommand {
170    /// Prompt text.
171    #[arg(short = 'p', long = "prompt")]
172    pub prompt: Option<String>,
173    /// Positional prompt text.
174    pub prompt_parts: Vec<String>,
175    /// Append a run to the selected session.
176    #[arg(short = 's', conflicts_with_all = ["new_session", "continue_session"], long)]
177    pub session: Option<String>,
178    /// Continue the latest local session.
179    #[arg(long = "continue", conflicts_with = "new_session")]
180    pub continue_session: bool,
181    /// Create a fresh session.
182    #[arg(long)]
183    pub new_session: bool,
184    /// Restore from a specific run before appending a run.
185    #[arg(long)]
186    pub run: Option<String>,
187    /// Branch from a specific run before appending a run.
188    #[arg(long, conflicts_with = "run")]
189    pub branch_from: Option<String>,
190    /// Agent profile name or YAML path.
191    #[arg(long)]
192    pub profile: Option<String>,
193    /// Enable worker mode or set an optional worker label.
194    #[arg(long, num_args = 0..=1, default_missing_value = "true")]
195    pub worker: Option<String>,
196    /// Explicit worker label.
197    #[arg(long = "worker-label")]
198    pub worker_label: Option<String>,
199    /// Enable a git worktree or set an optional worktree name/path.
200    #[arg(short = 'w', long, num_args = 0..=1, default_missing_value = "true")]
201    pub worktree: Option<String>,
202    /// Explicit worktree name/path.
203    #[arg(long = "worktree-name")]
204    pub worktree_name: Option<String>,
205    /// Git branch for worktree metadata.
206    #[arg(long)]
207    pub branch: Option<String>,
208    /// Output mode.
209    #[arg(long)]
210    pub output: Option<OutputMode>,
211    /// Headless human-in-the-loop policy.
212    #[arg(long)]
213    pub hitl: Option<HitlPolicy>,
214    /// Internal runtime goal-mode options.
215    #[arg(skip)]
216    pub goal: Option<GoalCommandOptions>,
217    /// Internal stable provider-routing affinity id.
218    #[arg(skip)]
219    pub session_affinity_id: Option<String>,
220    /// Internal host RPC environment attachments.
221    #[arg(skip)]
222    pub environment_attachments: Vec<starweaver_rpc_core::EnvironmentAttachmentRef>,
223}
224
225/// Internal goal-mode options attached by product surfaces such as the TUI.
226#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct GoalCommandOptions {
228    /// Goal objective.
229    pub objective: String,
230    /// Maximum runtime goal retry iterations.
231    pub max_iterations: usize,
232}
233
234impl RunCommand {
235    /// Return prompt text.
236    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/// Compact session commands.
252#[derive(Clone, Debug, Subcommand)]
253pub enum SessionCommand {
254    /// List local sessions.
255    List(SessionListCommand),
256    /// Show one session with recent runs.
257    Show(SessionShowCommand),
258    /// Replay stored display messages.
259    Replay(SessionReplayCommand),
260    /// Delete one local session and its retained evidence.
261    Delete(SessionDeleteCommand),
262    /// Trim retained run evidence.
263    Trim(SessionTrimCommand),
264}
265
266/// Session list command.
267#[derive(Clone, Debug, Args)]
268pub struct SessionListCommand {
269    /// Output mode.
270    #[arg(long, default_value = "display-jsonl")]
271    pub output: OutputMode,
272    /// Maximum sessions to show.
273    #[arg(long, default_value_t = 50)]
274    pub limit: usize,
275}
276
277/// Session show command.
278#[derive(Clone, Debug, Args)]
279pub struct SessionShowCommand {
280    /// Session id.
281    pub session_id: String,
282    /// Output mode.
283    #[arg(long, default_value = "display-jsonl")]
284    pub output: OutputMode,
285    /// Recent run limit.
286    #[arg(long, default_value_t = 20)]
287    pub runs: usize,
288}
289
290/// Session replay command.
291#[derive(Clone, Debug, Args)]
292pub struct SessionReplayCommand {
293    /// Session id.
294    pub session_id: String,
295    /// Optional run id.
296    #[arg(long)]
297    pub run: Option<String>,
298    /// Cursor sequence to replay after.
299    #[arg(long)]
300    pub after: Option<usize>,
301    /// Output mode.
302    #[arg(long, default_value = "display-jsonl")]
303    pub output: OutputMode,
304}
305
306/// Session delete command.
307#[derive(Clone, Debug, Args)]
308pub struct SessionDeleteCommand {
309    /// Session id or unique prefix.
310    pub session_id: String,
311    /// Confirm deletion.
312    #[arg(long)]
313    pub yes: bool,
314    /// Output mode.
315    #[arg(long, default_value = "text")]
316    pub output: OutputMode,
317}
318
319/// Session trim command.
320#[derive(Clone, Debug, Args)]
321pub struct SessionTrimCommand {
322    /// Trim current session.
323    #[arg(long)]
324    pub current: bool,
325    /// Trim all sessions.
326    #[arg(long)]
327    pub all: bool,
328    /// Trim a selected session.
329    #[arg(long)]
330    pub session: Option<String>,
331    /// Retain this many recent runs per session.
332    #[arg(long, default_value_t = 20)]
333    pub keep_runs: usize,
334    /// Trim runs older than a duration such as 7d, 24h, or 3600s.
335    #[arg(long)]
336    pub older_than: Option<String>,
337    /// Preview trim results.
338    #[arg(long)]
339    pub dry_run: bool,
340    /// Output mode.
341    #[arg(long, default_value = "display-jsonl")]
342    pub output: OutputMode,
343}
344
345/// Profile commands.
346#[derive(Clone, Debug, Subcommand)]
347pub enum ProfileCommand {
348    /// List built-in and configured profiles.
349    List,
350    /// Show one built-in or configured profile.
351    Show { name: String },
352}
353
354/// Setup command.
355#[derive(Clone, Debug, Args)]
356pub struct SetupCommand {
357    /// Initialize global configuration only.
358    #[arg(long, conflicts_with = "project")]
359    pub global: bool,
360    /// Initialize project configuration only.
361    #[arg(long)]
362    pub project: bool,
363    /// Replace existing generated files.
364    #[arg(long)]
365    pub force: bool,
366}
367
368/// Auth commands.
369#[derive(Clone, Debug, Subcommand)]
370pub enum AuthCommand {
371    /// Log in to an OAuth provider.
372    Login(AuthProviderCommand),
373    /// Print provider auth status.
374    Status(AuthStatusCommand),
375    /// Refresh provider credentials.
376    Refresh(AuthProviderCommand),
377    /// Remove provider credentials from the local auth store.
378    Logout(AuthLogoutCommand),
379    /// Inspect OAuth store health without printing tokens.
380    Doctor(AuthDoctorCommand),
381}
382
383/// Provider-scoped auth command.
384#[derive(Clone, Debug, Args)]
385pub struct AuthProviderCommand {
386    /// Provider name.
387    #[arg(default_value = "codex", value_parser = ["codex"])]
388    pub provider: String,
389    /// Auth file path. Defaults to ~/.starweaver/auth.json.
390    #[arg(long = "auth-file")]
391    pub auth_file: Option<String>,
392    /// Device authorization timeout in seconds.
393    #[arg(long, default_value_t = 15 * 60)]
394    pub timeout_seconds: u64,
395}
396
397/// Auth status command.
398#[derive(Clone, Debug, Args)]
399pub struct AuthStatusCommand {
400    /// Provider name.
401    #[arg(default_value = "codex", value_parser = ["codex"])]
402    pub provider: Option<String>,
403    /// Auth file path. Defaults to ~/.starweaver/auth.json.
404    #[arg(long = "auth-file")]
405    pub auth_file: Option<String>,
406}
407
408/// Auth logout command.
409#[derive(Clone, Debug, Args)]
410pub struct AuthLogoutCommand {
411    /// Provider name.
412    #[arg(default_value = "codex", value_parser = ["codex"])]
413    pub provider: String,
414    /// Auth file path. Defaults to ~/.starweaver/auth.json.
415    #[arg(long = "auth-file")]
416    pub auth_file: Option<String>,
417    /// Revoke provider tokens before deleting local credentials.
418    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
419    pub revoke: bool,
420}
421
422/// Auth doctor command.
423#[derive(Clone, Debug, Args)]
424pub struct AuthDoctorCommand {
425    /// Auth file path. Defaults to ~/.starweaver/auth.json.
426    #[arg(long = "auth-file")]
427    pub auth_file: Option<String>,
428}
429
430/// Catalog inspection commands.
431#[derive(Clone, Debug, Subcommand)]
432pub enum CatalogCommand {
433    /// List configured entries.
434    List,
435    /// Show one configured entry.
436    Show { name: String },
437    /// Validate configured entries and print findings.
438    Doctor,
439}
440
441/// Tool catalog commands.
442#[derive(Clone, Debug, Subcommand)]
443pub enum ToolsCommand {
444    /// List default first-party tools.
445    List,
446    /// Validate tool policy and default catalog.
447    Doctor,
448}
449
450/// TUI command.
451#[derive(Clone, Debug, Args)]
452pub struct TuiCommand {
453    /// Session id to render. Omit for a clean welcome screen.
454    #[arg(long)]
455    pub session: Option<String>,
456    /// Optional run id to render.
457    #[arg(long)]
458    pub run: Option<String>,
459    /// Render only messages after this display cursor.
460    #[arg(long)]
461    pub after: Option<usize>,
462    /// Force interactive terminal UI when stdout is a TTY.
463    #[arg(long)]
464    pub interactive: bool,
465    /// Force deterministic snapshot output for scripts and tests.
466    #[arg(long, conflicts_with = "interactive")]
467    pub snapshot: bool,
468    /// Output mode for non-interactive TUI snapshots.
469    #[arg(long, default_value = "text")]
470    pub output: OutputMode,
471}
472
473/// Approval commands.
474#[derive(Clone, Debug, Subcommand)]
475pub enum ApprovalCommand {
476    /// List persisted approval records.
477    List(ApprovalListCommand),
478    /// Show one approval record.
479    Show { approval_id: String },
480    /// Approve one pending approval record.
481    Approve(ApprovalDecisionCommand),
482    /// Reject one pending approval record.
483    Reject(ApprovalDecisionCommand),
484}
485
486/// Approval list command.
487#[derive(Clone, Debug, Args)]
488pub struct ApprovalListCommand {
489    /// Filter by session id.
490    #[arg(long)]
491    pub session: Option<String>,
492    /// Filter by run id.
493    #[arg(long)]
494    pub run: Option<String>,
495    /// Output mode.
496    #[arg(long, default_value = "display-jsonl")]
497    pub output: OutputMode,
498}
499
500/// Approval decision command.
501#[derive(Clone, Debug, Args)]
502pub struct ApprovalDecisionCommand {
503    /// Approval id.
504    pub approval_id: String,
505    /// Decision reason.
506    #[arg(long)]
507    pub reason: Option<String>,
508    /// Output mode.
509    #[arg(long, default_value = "text")]
510    pub output: OutputMode,
511}
512
513/// Deferred tool commands.
514#[derive(Clone, Debug, Subcommand)]
515pub enum DeferredCommand {
516    /// List persisted deferred tool records.
517    List(DeferredListCommand),
518    /// Show one deferred tool record.
519    Show { deferred_id: String },
520    /// Complete one deferred tool record with a JSON result payload.
521    Complete(DeferredCompleteCommand),
522    /// Fail one deferred tool record with an error message.
523    Fail(DeferredFailCommand),
524}
525
526/// Deferred list command.
527#[derive(Clone, Debug, Args)]
528pub struct DeferredListCommand {
529    /// Filter by session id.
530    #[arg(long)]
531    pub session: Option<String>,
532    /// Filter by run id.
533    #[arg(long)]
534    pub run: Option<String>,
535    /// Output mode.
536    #[arg(long, default_value = "display-jsonl")]
537    pub output: OutputMode,
538}
539
540/// Deferred complete command.
541#[derive(Clone, Debug, Args)]
542pub struct DeferredCompleteCommand {
543    /// Deferred id.
544    pub deferred_id: String,
545    /// JSON result payload.
546    #[arg(long)]
547    pub result: String,
548    /// Output mode.
549    #[arg(long, default_value = "text")]
550    pub output: OutputMode,
551}
552
553/// Deferred failure command.
554#[derive(Clone, Debug, Args)]
555pub struct DeferredFailCommand {
556    /// Deferred id.
557    pub deferred_id: String,
558    /// Error message.
559    #[arg(long)]
560    pub error: String,
561    /// Output mode.
562    #[arg(long, default_value = "text")]
563    pub output: OutputMode,
564}
565
566/// Resume command.
567#[derive(Clone, Debug, Args)]
568pub struct ResumeCommand {
569    /// Session id to resume. Defaults to current or latest session.
570    #[arg(long)]
571    pub session: Option<String>,
572    /// Run id to resume from. Defaults to the session active or head run.
573    #[arg(long)]
574    pub run: Option<String>,
575    /// Prompt to append for the continuation run.
576    #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
577    pub prompt: String,
578    /// Output mode.
579    #[arg(long)]
580    pub output: Option<OutputMode>,
581    /// Headless human-in-the-loop policy.
582    #[arg(long)]
583    pub hitl: Option<HitlPolicy>,
584}
585
586/// Reset command.
587#[derive(Clone, Debug, Args)]
588pub struct ResetCommand {
589    /// Confirm runtime state removal.
590    #[arg(long)]
591    pub yes: bool,
592    /// Output mode.
593    #[arg(long, default_value = "text")]
594    pub output: OutputMode,
595}
596
597/// RPC runtime command.
598#[derive(Clone, Debug, Args)]
599pub struct RpcCommand {
600    /// Runtime transport.
601    #[arg(default_value = "stdio")]
602    pub transport: RpcTransport,
603    /// HTTP bind host.
604    #[arg(long, default_value = "127.0.0.1")]
605    pub host: String,
606    /// HTTP bind port.
607    #[arg(long, default_value_t = 8765)]
608    pub port: u16,
609}
610
611/// RPC runtime transport.
612#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
613#[serde(rename_all = "kebab-case")]
614pub enum RpcTransport {
615    /// Newline-delimited JSON-RPC over stdin/stdout.
616    #[default]
617    Stdio,
618    /// JSON-RPC request/response over HTTP.
619    Http,
620}
621
622/// Update command.
623#[derive(Clone, Debug, Args)]
624pub struct UpdateCommand {
625    /// Update target, defaults to cli.
626    #[arg(default_value = "cli")]
627    pub target: String,
628    /// Print the update plan without downloading or installing.
629    #[arg(long)]
630    pub dry_run: bool,
631    /// Reinstall even when the selected release matches the current version.
632    #[arg(long, short = 'f')]
633    pub force: bool,
634}
635
636/// Config commands.
637#[derive(Clone, Debug, Subcommand)]
638pub enum ConfigCommand {
639    /// Initialize a Starweaver config file.
640    Init {
641        /// Write the global config file.
642        #[arg(long, conflicts_with = "project")]
643        global: bool,
644        /// Write the project config file.
645        #[arg(long)]
646        project: bool,
647        /// Replace an existing config file.
648        #[arg(long)]
649        force: bool,
650    },
651    /// Get a resolved config value.
652    Get { key: String },
653    /// Set a config value.
654    Set {
655        /// Write the global config file.
656        #[arg(long, conflicts_with = "project")]
657        global: bool,
658        /// Write the project config file.
659        #[arg(long)]
660        project: bool,
661        /// Config key.
662        key: String,
663        /// Config value.
664        value: String,
665    },
666}
667
668/// Output mode.
669#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
670#[serde(rename_all = "kebab-case")]
671pub enum OutputMode {
672    /// Human-readable text.
673    Text,
674    /// Starweaver durable `DisplayMessage` JSON lines.
675    #[default]
676    DisplayJsonl,
677    /// Starweaver/AGUI top-level event JSON lines.
678    AguiJsonl,
679    /// Compact JSON command result.
680    Json,
681    /// Persist and print compact status.
682    Silent,
683}
684
685/// HITL policy.
686#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
687#[serde(rename_all = "snake_case")]
688pub enum HitlPolicy {
689    /// Deny approvals.
690    Deny,
691    /// Defer approvals.
692    Defer,
693    /// Fail on approvals.
694    Fail,
695    /// Prompt interactively.
696    #[default]
697    Prompt,
698}
699
700/// Build the clap command schema.
701#[must_use]
702pub fn command() -> clap::Command {
703    Cli::command()
704}
705
706/// Parse CLI arguments.
707pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
708    Cli::try_parse_from(args).map_err(|error| clap_error(&error))
709}
710
711/// Parse CLI arguments from OS strings.
712#[allow(dead_code)]
713pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
714    Cli::try_parse_from(args).map_err(|error| clap_error(&error))
715}
716
717fn clap_error(error: &clap::Error) -> CliError {
718    match error.kind() {
719        clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
720            CliError::Display(error.to_string())
721        }
722        _ => CliError::Usage(error.to_string()),
723    }
724}