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