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