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}
221
222/// Internal goal-mode options attached by product surfaces such as the TUI.
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct GoalCommandOptions {
225    /// Goal objective.
226    pub objective: String,
227    /// Maximum runtime goal retry iterations.
228    pub max_iterations: usize,
229}
230
231impl RunCommand {
232    /// Return prompt text.
233    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/// Compact session commands.
249#[derive(Clone, Debug, Subcommand)]
250pub enum SessionCommand {
251    /// List local sessions.
252    List(SessionListCommand),
253    /// Show one session with recent runs.
254    Show(SessionShowCommand),
255    /// Replay stored display messages.
256    Replay(SessionReplayCommand),
257    /// Delete one local session and its retained evidence.
258    Delete(SessionDeleteCommand),
259    /// Trim retained run evidence.
260    Trim(SessionTrimCommand),
261}
262
263/// Session list command.
264#[derive(Clone, Debug, Args)]
265pub struct SessionListCommand {
266    /// Output mode.
267    #[arg(long, default_value = "display-jsonl")]
268    pub output: OutputMode,
269    /// Maximum sessions to show.
270    #[arg(long, default_value_t = 50)]
271    pub limit: usize,
272}
273
274/// Session show command.
275#[derive(Clone, Debug, Args)]
276pub struct SessionShowCommand {
277    /// Session id.
278    pub session_id: String,
279    /// Output mode.
280    #[arg(long, default_value = "display-jsonl")]
281    pub output: OutputMode,
282    /// Recent run limit.
283    #[arg(long, default_value_t = 20)]
284    pub runs: usize,
285}
286
287/// Session replay command.
288#[derive(Clone, Debug, Args)]
289pub struct SessionReplayCommand {
290    /// Session id.
291    pub session_id: String,
292    /// Optional run id.
293    #[arg(long)]
294    pub run: Option<String>,
295    /// Cursor sequence to replay after.
296    #[arg(long)]
297    pub after: Option<usize>,
298    /// Output mode.
299    #[arg(long, default_value = "display-jsonl")]
300    pub output: OutputMode,
301}
302
303/// Session delete command.
304#[derive(Clone, Debug, Args)]
305pub struct SessionDeleteCommand {
306    /// Session id or unique prefix.
307    pub session_id: String,
308    /// Confirm deletion.
309    #[arg(long)]
310    pub yes: bool,
311    /// Output mode.
312    #[arg(long, default_value = "text")]
313    pub output: OutputMode,
314}
315
316/// Session trim command.
317#[derive(Clone, Debug, Args)]
318pub struct SessionTrimCommand {
319    /// Trim current session.
320    #[arg(long)]
321    pub current: bool,
322    /// Trim all sessions.
323    #[arg(long)]
324    pub all: bool,
325    /// Trim a selected session.
326    #[arg(long)]
327    pub session: Option<String>,
328    /// Retain this many recent runs per session.
329    #[arg(long, default_value_t = 20)]
330    pub keep_runs: usize,
331    /// Trim runs older than a duration such as 7d, 24h, or 3600s.
332    #[arg(long)]
333    pub older_than: Option<String>,
334    /// Preview trim results.
335    #[arg(long)]
336    pub dry_run: bool,
337    /// Output mode.
338    #[arg(long, default_value = "display-jsonl")]
339    pub output: OutputMode,
340}
341
342/// Profile commands.
343#[derive(Clone, Debug, Subcommand)]
344pub enum ProfileCommand {
345    /// List built-in and configured profiles.
346    List,
347    /// Show one built-in or configured profile.
348    Show { name: String },
349}
350
351/// Setup command.
352#[derive(Clone, Debug, Args)]
353pub struct SetupCommand {
354    /// Initialize global configuration only.
355    #[arg(long, conflicts_with = "project")]
356    pub global: bool,
357    /// Initialize project configuration only.
358    #[arg(long)]
359    pub project: bool,
360    /// Replace existing generated files.
361    #[arg(long)]
362    pub force: bool,
363}
364
365/// Auth commands.
366#[derive(Clone, Debug, Subcommand)]
367pub enum AuthCommand {
368    /// Log in to an OAuth provider.
369    Login(AuthProviderCommand),
370    /// Print provider auth status.
371    Status(AuthStatusCommand),
372    /// Refresh provider credentials.
373    Refresh(AuthProviderCommand),
374    /// Remove provider credentials from the local auth store.
375    Logout(AuthLogoutCommand),
376    /// Inspect OAuth store health without printing tokens.
377    Doctor(AuthDoctorCommand),
378}
379
380/// Provider-scoped auth command.
381#[derive(Clone, Debug, Args)]
382pub struct AuthProviderCommand {
383    /// Provider name.
384    #[arg(default_value = "codex", value_parser = ["codex"])]
385    pub provider: String,
386    /// Auth file path. Defaults to ~/.starweaver/auth.json.
387    #[arg(long = "auth-file")]
388    pub auth_file: Option<String>,
389    /// Device authorization timeout in seconds.
390    #[arg(long, default_value_t = 15 * 60)]
391    pub timeout_seconds: u64,
392}
393
394/// Auth status command.
395#[derive(Clone, Debug, Args)]
396pub struct AuthStatusCommand {
397    /// Provider name.
398    #[arg(default_value = "codex", value_parser = ["codex"])]
399    pub provider: Option<String>,
400    /// Auth file path. Defaults to ~/.starweaver/auth.json.
401    #[arg(long = "auth-file")]
402    pub auth_file: Option<String>,
403}
404
405/// Auth logout command.
406#[derive(Clone, Debug, Args)]
407pub struct AuthLogoutCommand {
408    /// Provider name.
409    #[arg(default_value = "codex", value_parser = ["codex"])]
410    pub provider: String,
411    /// Auth file path. Defaults to ~/.starweaver/auth.json.
412    #[arg(long = "auth-file")]
413    pub auth_file: Option<String>,
414    /// Revoke provider tokens before deleting local credentials.
415    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
416    pub revoke: bool,
417}
418
419/// Auth doctor command.
420#[derive(Clone, Debug, Args)]
421pub struct AuthDoctorCommand {
422    /// Auth file path. Defaults to ~/.starweaver/auth.json.
423    #[arg(long = "auth-file")]
424    pub auth_file: Option<String>,
425}
426
427/// Catalog inspection commands.
428#[derive(Clone, Debug, Subcommand)]
429pub enum CatalogCommand {
430    /// List configured entries.
431    List,
432    /// Show one configured entry.
433    Show { name: String },
434    /// Validate configured entries and print findings.
435    Doctor,
436}
437
438/// Tool catalog commands.
439#[derive(Clone, Debug, Subcommand)]
440pub enum ToolsCommand {
441    /// List default first-party tools.
442    List,
443    /// Validate tool policy and default catalog.
444    Doctor,
445}
446
447/// TUI command.
448#[derive(Clone, Debug, Args)]
449pub struct TuiCommand {
450    /// Session id to render. Omit for a clean welcome screen.
451    #[arg(long)]
452    pub session: Option<String>,
453    /// Optional run id to render.
454    #[arg(long)]
455    pub run: Option<String>,
456    /// Render only messages after this display cursor.
457    #[arg(long)]
458    pub after: Option<usize>,
459    /// Force interactive terminal UI when stdout is a TTY.
460    #[arg(long)]
461    pub interactive: bool,
462    /// Force deterministic snapshot output for scripts and tests.
463    #[arg(long, conflicts_with = "interactive")]
464    pub snapshot: bool,
465    /// Output mode for non-interactive TUI snapshots.
466    #[arg(long, default_value = "text")]
467    pub output: OutputMode,
468}
469
470/// Approval commands.
471#[derive(Clone, Debug, Subcommand)]
472pub enum ApprovalCommand {
473    /// List persisted approval records.
474    List(ApprovalListCommand),
475    /// Show one approval record.
476    Show { approval_id: String },
477    /// Approve one pending approval record.
478    Approve(ApprovalDecisionCommand),
479    /// Reject one pending approval record.
480    Reject(ApprovalDecisionCommand),
481}
482
483/// Approval list command.
484#[derive(Clone, Debug, Args)]
485pub struct ApprovalListCommand {
486    /// Filter by session id.
487    #[arg(long)]
488    pub session: Option<String>,
489    /// Filter by run id.
490    #[arg(long)]
491    pub run: Option<String>,
492    /// Output mode.
493    #[arg(long, default_value = "display-jsonl")]
494    pub output: OutputMode,
495}
496
497/// Approval decision command.
498#[derive(Clone, Debug, Args)]
499pub struct ApprovalDecisionCommand {
500    /// Approval id.
501    pub approval_id: String,
502    /// Decision reason.
503    #[arg(long)]
504    pub reason: Option<String>,
505    /// Output mode.
506    #[arg(long, default_value = "text")]
507    pub output: OutputMode,
508}
509
510/// Deferred tool commands.
511#[derive(Clone, Debug, Subcommand)]
512pub enum DeferredCommand {
513    /// List persisted deferred tool records.
514    List(DeferredListCommand),
515    /// Show one deferred tool record.
516    Show { deferred_id: String },
517    /// Complete one deferred tool record with a JSON result payload.
518    Complete(DeferredCompleteCommand),
519    /// Fail one deferred tool record with an error message.
520    Fail(DeferredFailCommand),
521}
522
523/// Deferred list command.
524#[derive(Clone, Debug, Args)]
525pub struct DeferredListCommand {
526    /// Filter by session id.
527    #[arg(long)]
528    pub session: Option<String>,
529    /// Filter by run id.
530    #[arg(long)]
531    pub run: Option<String>,
532    /// Output mode.
533    #[arg(long, default_value = "display-jsonl")]
534    pub output: OutputMode,
535}
536
537/// Deferred complete command.
538#[derive(Clone, Debug, Args)]
539pub struct DeferredCompleteCommand {
540    /// Deferred id.
541    pub deferred_id: String,
542    /// JSON result payload.
543    #[arg(long)]
544    pub result: String,
545    /// Output mode.
546    #[arg(long, default_value = "text")]
547    pub output: OutputMode,
548}
549
550/// Deferred failure command.
551#[derive(Clone, Debug, Args)]
552pub struct DeferredFailCommand {
553    /// Deferred id.
554    pub deferred_id: String,
555    /// Error message.
556    #[arg(long)]
557    pub error: String,
558    /// Output mode.
559    #[arg(long, default_value = "text")]
560    pub output: OutputMode,
561}
562
563/// Resume command.
564#[derive(Clone, Debug, Args)]
565pub struct ResumeCommand {
566    /// Session id to resume. Defaults to current or latest session.
567    #[arg(long)]
568    pub session: Option<String>,
569    /// Run id to resume from. Defaults to the session active or head run.
570    #[arg(long)]
571    pub run: Option<String>,
572    /// Prompt to append for the continuation run.
573    #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
574    pub prompt: String,
575    /// Output mode.
576    #[arg(long)]
577    pub output: Option<OutputMode>,
578    /// Headless human-in-the-loop policy.
579    #[arg(long)]
580    pub hitl: Option<HitlPolicy>,
581}
582
583/// Reset command.
584#[derive(Clone, Debug, Args)]
585pub struct ResetCommand {
586    /// Confirm runtime state removal.
587    #[arg(long)]
588    pub yes: bool,
589    /// Output mode.
590    #[arg(long, default_value = "text")]
591    pub output: OutputMode,
592}
593
594/// RPC runtime command.
595#[derive(Clone, Debug, Args)]
596pub struct RpcCommand {
597    /// Runtime transport.
598    #[arg(default_value = "stdio")]
599    pub transport: RpcTransport,
600    /// HTTP bind host.
601    #[arg(long, default_value = "127.0.0.1")]
602    pub host: String,
603    /// HTTP bind port.
604    #[arg(long, default_value_t = 8765)]
605    pub port: u16,
606}
607
608/// RPC runtime transport.
609#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
610#[serde(rename_all = "kebab-case")]
611pub enum RpcTransport {
612    /// Newline-delimited JSON-RPC over stdin/stdout.
613    #[default]
614    Stdio,
615    /// JSON-RPC request/response over HTTP.
616    Http,
617}
618
619/// Update command.
620#[derive(Clone, Debug, Args)]
621pub struct UpdateCommand {
622    /// Update target, defaults to cli.
623    #[arg(default_value = "cli")]
624    pub target: String,
625    /// Print the update plan without downloading or installing.
626    #[arg(long)]
627    pub dry_run: bool,
628    /// Reinstall even when the selected release matches the current version.
629    #[arg(long, short = 'f')]
630    pub force: bool,
631}
632
633/// Config commands.
634#[derive(Clone, Debug, Subcommand)]
635pub enum ConfigCommand {
636    /// Initialize a Starweaver config file.
637    Init {
638        /// Write the global config file.
639        #[arg(long, conflicts_with = "project")]
640        global: bool,
641        /// Write the project config file.
642        #[arg(long)]
643        project: bool,
644        /// Replace an existing config file.
645        #[arg(long)]
646        force: bool,
647    },
648    /// Get a resolved config value.
649    Get { key: String },
650    /// Set a config value.
651    Set {
652        /// Write the global config file.
653        #[arg(long, conflicts_with = "project")]
654        global: bool,
655        /// Write the project config file.
656        #[arg(long)]
657        project: bool,
658        /// Config key.
659        key: String,
660        /// Config value.
661        value: String,
662    },
663}
664
665/// Output mode.
666#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
667#[serde(rename_all = "kebab-case")]
668pub enum OutputMode {
669    /// Human-readable text.
670    Text,
671    /// Starweaver durable `DisplayMessage` JSON lines.
672    #[default]
673    DisplayJsonl,
674    /// Starweaver/AGUI top-level event JSON lines.
675    AguiJsonl,
676    /// Compact JSON command result.
677    Json,
678    /// Persist and print compact status.
679    Silent,
680}
681
682/// HITL policy.
683#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
684#[serde(rename_all = "snake_case")]
685pub enum HitlPolicy {
686    /// Deny approvals.
687    Deny,
688    /// Defer approvals.
689    Defer,
690    /// Fail on approvals.
691    Fail,
692    /// Prompt interactively.
693    #[default]
694    Prompt,
695}
696
697/// Build the clap command schema.
698#[must_use]
699pub fn command() -> clap::Command {
700    Cli::command()
701}
702
703/// Parse CLI arguments.
704pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
705    Cli::try_parse_from(args).map_err(|error| clap_error(&error))
706}
707
708/// Parse CLI arguments from OS strings.
709#[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}