Skip to main content

starweaver_cli/
args.rs

1//! CLI argument parsing.
2
3use std::{ffi::OsString, path::PathBuf};
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    /// Agent materialization semantics for a restored run.
42    #[arg(long, global = false, default_value = "preserve")]
43    pub continuation_mode: ContinuationModeArg,
44    /// Enable worker mode or set an optional worker label.
45    #[arg(long, global = false, num_args = 0..=1, default_missing_value = "true")]
46    pub worker: Option<String>,
47    /// Explicit worker label.
48    #[arg(long = "worker-label", global = false)]
49    pub worker_label: Option<String>,
50    /// Enable a git worktree or set an optional worktree name/path.
51    #[arg(
52        short = 'w',
53        long,
54        global = false,
55        num_args = 0..=1,
56        default_missing_value = "true"
57    )]
58    pub worktree: Option<String>,
59    /// Explicit worktree name/path.
60    #[arg(long = "worktree-name", global = false)]
61    pub worktree_name: Option<String>,
62    /// Git branch for worktree metadata.
63    #[arg(long, global = false)]
64    pub branch: Option<String>,
65    /// Output mode.
66    #[arg(long, global = false)]
67    pub output: Option<OutputMode>,
68    /// Headless human-in-the-loop policy for prompt shorthand.
69    #[arg(long, global = false)]
70    pub hitl: Option<HitlPolicy>,
71    /// Override local store database path.
72    #[arg(long, global = true)]
73    pub store: Option<String>,
74    /// Optional subcommand.
75    #[command(subcommand)]
76    pub command: Option<CliCommand>,
77}
78
79/// CLI command families.
80#[allow(clippy::large_enum_variant)]
81#[derive(Clone, Debug, Subcommand)]
82pub enum CliCommand {
83    /// Print SDK identity.
84    Version,
85    /// Run a prompt.
86    Run(RunCommand),
87    /// Manage local sessions.
88    Session {
89        /// Session subcommand.
90        #[command(subcommand)]
91        command: SessionCommand,
92    },
93    /// Manage canonical storage migrations and imports.
94    Storage {
95        /// Storage subcommand.
96        #[command(subcommand)]
97        command: StorageCommand,
98    },
99    /// Manage agent profiles.
100    Profile {
101        /// Profile subcommand.
102        #[command(subcommand)]
103        command: ProfileCommand,
104    },
105    /// Initialize local CLI configuration and catalogs.
106    Setup(SetupCommand),
107    /// Inspect OAuth-backed provider authentication.
108    Auth {
109        /// Auth subcommand.
110        #[command(subcommand)]
111        command: AuthCommand,
112    },
113    /// Inspect configured skills.
114    Skill {
115        /// Skill subcommand.
116        #[command(subcommand)]
117        command: CatalogCommand,
118    },
119    /// Inspect configured subagents.
120    Subagent {
121        /// Subagent subcommand.
122        #[command(subcommand)]
123        command: CatalogCommand,
124    },
125    /// Inspect configured MCP servers.
126    Mcp {
127        /// MCP subcommand.
128        #[command(subcommand)]
129        command: CatalogCommand,
130    },
131    /// Inspect default CLI tool catalog and policy.
132    Tools {
133        /// Tools subcommand.
134        #[command(subcommand)]
135        command: ToolsCommand,
136    },
137    /// Render a retained terminal UI from local session display messages.
138    Tui(TuiCommand),
139    /// Manage persisted approval requests.
140    Approval {
141        /// Approval subcommand.
142        #[command(subcommand)]
143        command: ApprovalCommand,
144    },
145    /// Manage deferred tool calls.
146    Deferred {
147        /// Deferred subcommand.
148        #[command(subcommand)]
149        command: DeferredCommand,
150    },
151    /// Resume a waiting session by appending a continuation run.
152    Resume(ResumeCommand),
153    /// Remove runtime session state while preserving configuration.
154    Reset(ResetCommand),
155    /// Print diagnostics.
156    Diagnostics,
157    /// Print replay-check guidance.
158    ReplayCheck,
159    /// Update installed Starweaver components.
160    Update(UpdateCommand),
161    /// Get or set configuration values.
162    Config {
163        /// Config subcommand.
164        #[command(subcommand)]
165        command: ConfigCommand,
166    },
167    /// Generate shell completion scripts.
168    Completion {
169        /// Target shell.
170        shell: Shell,
171    },
172}
173
174/// Prompt run command.
175#[derive(Clone, Debug, Args)]
176pub struct RunCommand {
177    /// Prompt text.
178    #[arg(short = 'p', long = "prompt")]
179    pub prompt: Option<String>,
180    /// Positional prompt text.
181    pub prompt_parts: Vec<String>,
182    /// Append a run to the selected session.
183    #[arg(short = 's', conflicts_with_all = ["new_session", "continue_session"], long)]
184    pub session: Option<String>,
185    /// Continue the latest local session.
186    #[arg(long = "continue", conflicts_with = "new_session")]
187    pub continue_session: bool,
188    /// Create a fresh session.
189    #[arg(long)]
190    pub new_session: bool,
191    /// Restore from a specific run before appending a run.
192    #[arg(long)]
193    pub run: Option<String>,
194    /// Branch from a specific run before appending a run.
195    #[arg(long, conflicts_with = "run")]
196    pub branch_from: Option<String>,
197    /// Agent profile name or YAML path.
198    #[arg(long)]
199    pub profile: Option<String>,
200    /// Agent materialization semantics for a restored run.
201    #[arg(long, default_value = "preserve")]
202    pub continuation_mode: ContinuationModeArg,
203    /// Enable worker mode or set an optional worker label.
204    #[arg(long, num_args = 0..=1, default_missing_value = "true")]
205    pub worker: Option<String>,
206    /// Explicit worker label.
207    #[arg(long = "worker-label")]
208    pub worker_label: Option<String>,
209    /// Enable a git worktree or set an optional worktree name/path.
210    #[arg(short = 'w', long, num_args = 0..=1, default_missing_value = "true")]
211    pub worktree: Option<String>,
212    /// Explicit worktree name/path.
213    #[arg(long = "worktree-name")]
214    pub worktree_name: Option<String>,
215    /// Git branch for worktree metadata.
216    #[arg(long)]
217    pub branch: Option<String>,
218    /// Output mode.
219    #[arg(long)]
220    pub output: Option<OutputMode>,
221    /// Headless human-in-the-loop policy.
222    #[arg(long)]
223    pub hitl: Option<HitlPolicy>,
224    /// Internal runtime goal-mode options.
225    #[arg(skip)]
226    pub goal: Option<GoalCommandOptions>,
227    /// Internal stable provider-routing affinity id.
228    #[arg(skip)]
229    pub session_affinity_id: Option<String>,
230    /// Internal CLI environment attachments.
231    #[arg(skip)]
232    pub(crate) environment_attachments: Vec<crate::environment::EnvironmentAttachmentRef>,
233    /// Internal marker requiring an exclusive durable HITL continuation claim.
234    #[arg(skip)]
235    pub hitl_resume: bool,
236}
237
238/// Internal goal-mode options attached by product surfaces such as the TUI.
239#[derive(Clone, Debug, Eq, PartialEq)]
240pub struct GoalCommandOptions {
241    /// Goal objective.
242    pub objective: String,
243    /// Maximum runtime goal retry iterations.
244    pub max_iterations: usize,
245}
246
247impl RunCommand {
248    /// Return prompt text.
249    pub fn prompt_text(&self) -> CliResult<String> {
250        let prompt = self
251            .prompt
252            .clone()
253            .unwrap_or_else(|| self.prompt_parts.join(" "));
254        if prompt.trim().is_empty() {
255            Err(CliError::Usage(
256                "usage: starweaver-cli run -p <prompt>".to_string(),
257            ))
258        } else {
259            Ok(prompt)
260        }
261    }
262}
263
264/// Canonical storage administration commands.
265#[derive(Clone, Debug, Subcommand)]
266pub enum StorageCommand {
267    /// Import a legacy project-local database incrementally and idempotently.
268    ImportLegacy(StorageImportLegacyCommand),
269}
270
271/// Explicit legacy project database import.
272#[derive(Clone, Debug, Args)]
273pub struct StorageImportLegacyCommand {
274    /// Legacy `SQLite` database to import. Defaults to the current project's legacy location.
275    #[arg(long)]
276    pub source: Option<PathBuf>,
277    /// Workspace identity assigned to imported sessions that do not already have one.
278    #[arg(long)]
279    pub workspace: Option<PathBuf>,
280    /// Output mode.
281    #[arg(long, default_value = "text")]
282    pub output: OutputMode,
283}
284
285/// Compact session commands.
286#[derive(Clone, Debug, Subcommand)]
287pub enum SessionCommand {
288    /// List local sessions.
289    List(SessionListCommand),
290    /// Search local sessions by metadata and approved text projections.
291    Search(SessionSearchCommand),
292    /// Show one session with recent runs.
293    Show(SessionShowCommand),
294    /// Replay stored display messages.
295    Replay(SessionReplayCommand),
296    /// Delete one local session and its retained evidence.
297    Delete(SessionDeleteCommand),
298    /// Trim retained run evidence.
299    Trim(SessionTrimCommand),
300}
301
302/// Session list command.
303#[derive(Clone, Debug, Args)]
304pub struct SessionListCommand {
305    /// Output mode.
306    #[arg(long, default_value = "display-jsonl")]
307    pub output: OutputMode,
308    /// Maximum sessions to show.
309    #[arg(long, default_value_t = 50)]
310    pub limit: usize,
311}
312
313/// Session search command.
314#[derive(Clone, Debug, Args)]
315pub struct SessionSearchCommand {
316    /// Optional case-insensitive literal text. Omit it to browse metadata.
317    pub text: Option<String>,
318    /// Exact session status.
319    #[arg(long)]
320    pub status: Option<SessionSearchStatusArg>,
321    /// Exact profile name.
322    #[arg(long)]
323    pub profile: Option<String>,
324    /// Exact workspace display value.
325    #[arg(long)]
326    pub workspace: Option<String>,
327    /// Search source; repeat to select multiple sources.
328    #[arg(long = "source", value_enum)]
329    pub sources: Vec<SessionSearchSourceArg>,
330    /// Result grouping level.
331    #[arg(long, value_enum, default_value = "session")]
332    pub granularity: SessionSearchGranularityArg,
333    /// Maximum hits to return.
334    #[arg(long, default_value_t = 20)]
335    pub limit: u32,
336    /// Opaque next-page cursor.
337    #[arg(long = "after")]
338    pub cursor: Option<String>,
339    /// Output mode.
340    #[arg(long, default_value = "text")]
341    pub output: OutputMode,
342}
343
344/// Session status accepted by search.
345#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
346#[value(rename_all = "snake_case")]
347pub enum SessionSearchStatusArg {
348    /// Active session.
349    Active,
350    /// Archived session.
351    Archived,
352    /// Failed session.
353    Failed,
354}
355
356/// Search source accepted by the CLI.
357#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
358#[value(rename_all = "snake_case")]
359pub enum SessionSearchSourceArg {
360    /// Session title and approved metadata.
361    SessionMetadata,
362    /// Canonical text input.
363    RunInput,
364    /// Bounded run output preview.
365    RunOutputPreview,
366    /// User-visible display messages.
367    #[value(alias = "display")]
368    DisplayMessage,
369}
370
371/// Search grouping accepted by the CLI.
372#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
373#[value(rename_all = "snake_case")]
374pub enum SessionSearchGranularityArg {
375    /// One hit per session.
376    Session,
377    /// One hit per session/run pair.
378    Run,
379    /// Every stable projected occurrence.
380    Occurrence,
381}
382
383/// Session show command.
384#[derive(Clone, Debug, Args)]
385pub struct SessionShowCommand {
386    /// Session id.
387    pub session_id: String,
388    /// Output mode.
389    #[arg(long, default_value = "display-jsonl")]
390    pub output: OutputMode,
391    /// Recent run limit.
392    #[arg(long, default_value_t = 20)]
393    pub runs: usize,
394}
395
396/// Session replay command.
397#[derive(Clone, Debug, Args)]
398pub struct SessionReplayCommand {
399    /// Session id.
400    pub session_id: String,
401    /// Optional run id.
402    #[arg(long)]
403    pub run: Option<String>,
404    /// Cursor sequence to replay after.
405    #[arg(long)]
406    pub after: Option<usize>,
407    /// Output mode.
408    #[arg(long, default_value = "display-jsonl")]
409    pub output: OutputMode,
410}
411
412/// Session delete command.
413#[derive(Clone, Debug, Args)]
414pub struct SessionDeleteCommand {
415    /// Session id or unique prefix.
416    pub session_id: String,
417    /// Confirm deletion.
418    #[arg(long)]
419    pub yes: bool,
420    /// Output mode.
421    #[arg(long, default_value = "text")]
422    pub output: OutputMode,
423}
424
425/// Session trim command.
426#[derive(Clone, Debug, Args)]
427pub struct SessionTrimCommand {
428    /// Trim current session.
429    #[arg(long)]
430    pub current: bool,
431    /// Trim all sessions.
432    #[arg(long)]
433    pub all: bool,
434    /// Trim a selected session.
435    #[arg(long)]
436    pub session: Option<String>,
437    /// Retain this many recent runs per session.
438    #[arg(long, default_value_t = 20)]
439    pub keep_runs: usize,
440    /// Trim runs older than a duration such as 7d, 24h, or 3600s.
441    #[arg(long)]
442    pub older_than: Option<String>,
443    /// Preview trim results.
444    #[arg(long)]
445    pub dry_run: bool,
446    /// Output mode.
447    #[arg(long, default_value = "display-jsonl")]
448    pub output: OutputMode,
449}
450
451/// Profile commands.
452#[derive(Clone, Debug, Subcommand)]
453pub enum ProfileCommand {
454    /// List built-in and configured profiles.
455    List,
456    /// Show one built-in or configured profile.
457    Show { name: String },
458}
459
460/// Setup command.
461#[derive(Clone, Debug, Args)]
462pub struct SetupCommand {
463    /// Initialize global configuration only.
464    #[arg(long, conflicts_with = "project")]
465    pub global: bool,
466    /// Initialize project configuration only.
467    #[arg(long)]
468    pub project: bool,
469    /// Replace existing generated files.
470    #[arg(long)]
471    pub force: bool,
472}
473
474/// Auth commands.
475#[derive(Clone, Debug, Subcommand)]
476pub enum AuthCommand {
477    /// Log in to an OAuth provider.
478    Login(AuthProviderCommand),
479    /// Print provider auth status.
480    Status(AuthStatusCommand),
481    /// Refresh provider credentials.
482    Refresh(AuthProviderCommand),
483    /// Remove provider credentials from the local auth store.
484    Logout(AuthLogoutCommand),
485    /// Inspect OAuth store health without printing tokens.
486    Doctor(AuthDoctorCommand),
487}
488
489/// Provider-scoped auth command.
490#[derive(Clone, Debug, Args)]
491pub struct AuthProviderCommand {
492    /// Provider name.
493    #[arg(default_value = "codex", value_parser = ["codex"])]
494    pub provider: String,
495    /// Auth file path. Defaults to ~/.starweaver/auth.json.
496    #[arg(long = "auth-file")]
497    pub auth_file: Option<String>,
498    /// Device authorization timeout in seconds.
499    #[arg(long, default_value_t = 15 * 60)]
500    pub timeout_seconds: u64,
501}
502
503/// Auth status command.
504#[derive(Clone, Debug, Args)]
505pub struct AuthStatusCommand {
506    /// Provider name.
507    #[arg(default_value = "codex", value_parser = ["codex"])]
508    pub provider: Option<String>,
509    /// Auth file path. Defaults to ~/.starweaver/auth.json.
510    #[arg(long = "auth-file")]
511    pub auth_file: Option<String>,
512}
513
514/// Auth logout command.
515#[derive(Clone, Debug, Args)]
516pub struct AuthLogoutCommand {
517    /// Provider name.
518    #[arg(default_value = "codex", value_parser = ["codex"])]
519    pub provider: String,
520    /// Auth file path. Defaults to ~/.starweaver/auth.json.
521    #[arg(long = "auth-file")]
522    pub auth_file: Option<String>,
523    /// Revoke provider tokens before deleting local credentials.
524    #[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
525    pub revoke: bool,
526}
527
528/// Auth doctor command.
529#[derive(Clone, Debug, Args)]
530pub struct AuthDoctorCommand {
531    /// Auth file path. Defaults to ~/.starweaver/auth.json.
532    #[arg(long = "auth-file")]
533    pub auth_file: Option<String>,
534}
535
536/// Catalog inspection commands.
537#[derive(Clone, Debug, Subcommand)]
538pub enum CatalogCommand {
539    /// List configured entries.
540    List,
541    /// Show one configured entry.
542    Show { name: String },
543    /// Validate configured entries and print findings.
544    Doctor,
545}
546
547/// Tool catalog commands.
548#[derive(Clone, Debug, Subcommand)]
549pub enum ToolsCommand {
550    /// List default first-party tools.
551    List,
552    /// Validate tool policy and default catalog.
553    Doctor,
554}
555
556/// TUI command.
557#[derive(Clone, Debug, Args)]
558pub struct TuiCommand {
559    /// Session id to render. Omit for a clean welcome screen.
560    #[arg(long)]
561    pub session: Option<String>,
562    /// Optional run id to render.
563    #[arg(long)]
564    pub run: Option<String>,
565    /// Render only messages after this display cursor.
566    #[arg(long)]
567    pub after: Option<usize>,
568    /// Force interactive terminal UI when stdout is a TTY.
569    #[arg(long)]
570    pub interactive: bool,
571    /// Force deterministic snapshot output for scripts and tests.
572    #[arg(long, conflicts_with = "interactive")]
573    pub snapshot: bool,
574    /// Output mode for non-interactive TUI snapshots.
575    #[arg(long, default_value = "text")]
576    pub output: OutputMode,
577    /// Transcript rendering mode for interactive TUI.
578    #[arg(long = "render-mode")]
579    pub render_mode: Option<TuiRenderMode>,
580}
581
582/// Approval commands.
583#[derive(Clone, Debug, Subcommand)]
584pub enum ApprovalCommand {
585    /// List persisted approval records.
586    List(ApprovalListCommand),
587    /// Show one approval record.
588    Show { approval_id: String },
589    /// Approve one pending approval record.
590    Approve(ApprovalDecisionCommand),
591    /// Reject one pending approval record.
592    Reject(ApprovalDecisionCommand),
593}
594
595/// Approval list command.
596#[derive(Clone, Debug, Args)]
597pub struct ApprovalListCommand {
598    /// Filter by session id.
599    #[arg(long)]
600    pub session: Option<String>,
601    /// Filter by run id.
602    #[arg(long)]
603    pub run: Option<String>,
604    /// Output mode.
605    #[arg(long, default_value = "display-jsonl")]
606    pub output: OutputMode,
607}
608
609/// Approval decision command.
610#[derive(Clone, Debug, Args)]
611pub struct ApprovalDecisionCommand {
612    /// Approval id.
613    pub approval_id: String,
614    /// Decision reason.
615    #[arg(long)]
616    pub reason: Option<String>,
617    /// Output mode.
618    #[arg(long, default_value = "text")]
619    pub output: OutputMode,
620}
621
622/// Deferred tool commands.
623#[derive(Clone, Debug, Subcommand)]
624pub enum DeferredCommand {
625    /// List persisted deferred tool records.
626    List(DeferredListCommand),
627    /// Show one deferred tool record.
628    Show { deferred_id: String },
629    /// Complete one deferred tool record with a JSON result payload.
630    Complete(DeferredCompleteCommand),
631    /// Fail one deferred tool record with an error message.
632    Fail(DeferredFailCommand),
633}
634
635/// Deferred list command.
636#[derive(Clone, Debug, Args)]
637pub struct DeferredListCommand {
638    /// Filter by session id.
639    #[arg(long)]
640    pub session: Option<String>,
641    /// Filter by run id.
642    #[arg(long)]
643    pub run: Option<String>,
644    /// Output mode.
645    #[arg(long, default_value = "display-jsonl")]
646    pub output: OutputMode,
647}
648
649/// Deferred complete command.
650#[derive(Clone, Debug, Args)]
651pub struct DeferredCompleteCommand {
652    /// Deferred id.
653    pub deferred_id: String,
654    /// JSON result payload.
655    #[arg(long)]
656    pub result: String,
657    /// Output mode.
658    #[arg(long, default_value = "text")]
659    pub output: OutputMode,
660}
661
662/// Deferred failure command.
663#[derive(Clone, Debug, Args)]
664pub struct DeferredFailCommand {
665    /// Deferred id.
666    pub deferred_id: String,
667    /// Error message.
668    #[arg(long)]
669    pub error: String,
670    /// Output mode.
671    #[arg(long, default_value = "text")]
672    pub output: OutputMode,
673}
674
675/// Resume command.
676#[derive(Clone, Debug, Args)]
677pub struct ResumeCommand {
678    /// Session id to resume. Defaults to current or latest session.
679    #[arg(long)]
680    pub session: Option<String>,
681    /// Run id to resume from. Defaults to the session active or head run.
682    #[arg(long)]
683    pub run: Option<String>,
684    /// Prompt to append for the continuation run.
685    #[arg(short = 'p', long = "prompt", default_value = "resume waiting run")]
686    pub prompt: String,
687    /// Output mode.
688    #[arg(long)]
689    pub output: Option<OutputMode>,
690    /// Headless human-in-the-loop policy.
691    #[arg(long)]
692    pub hitl: Option<HitlPolicy>,
693    /// Agent materialization semantics for the continuation.
694    #[arg(long, default_value = "preserve")]
695    pub continuation_mode: ContinuationModeArg,
696}
697
698/// Explicit continuation materialization mode.
699#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
700#[serde(rename_all = "snake_case")]
701pub enum ContinuationModeArg {
702    /// Require the exact source materialization fingerprint.
703    #[default]
704    Preserve,
705    /// Permit only an `AgentSpec` revision with otherwise equivalent bindings.
706    Compatible,
707    /// Deliberately accept and report all materialization drift.
708    Switch,
709}
710
711impl From<ContinuationModeArg> for starweaver_agent::ContinuationMaterializationMode {
712    fn from(value: ContinuationModeArg) -> Self {
713        match value {
714            ContinuationModeArg::Preserve => Self::Preserve,
715            ContinuationModeArg::Compatible => Self::Compatible,
716            ContinuationModeArg::Switch => Self::Switch,
717        }
718    }
719}
720
721/// Reset command.
722#[derive(Clone, Debug, Args)]
723pub struct ResetCommand {
724    /// Confirm runtime state removal.
725    #[arg(long)]
726    pub yes: bool,
727    /// Output mode.
728    #[arg(long, default_value = "text")]
729    pub output: OutputMode,
730}
731
732/// Update command.
733#[derive(Clone, Debug, Args)]
734pub struct UpdateCommand {
735    /// Update target, defaults to cli.
736    #[arg(default_value = "cli")]
737    pub target: String,
738    /// Print the update plan without downloading or installing.
739    #[arg(long)]
740    pub dry_run: bool,
741    /// Reinstall even when the selected release matches the current version.
742    #[arg(long, short = 'f')]
743    pub force: bool,
744}
745
746/// Config commands.
747#[derive(Clone, Debug, Subcommand)]
748pub enum ConfigCommand {
749    /// Initialize a Starweaver config file.
750    Init {
751        /// Write the global config file.
752        #[arg(long, conflicts_with = "project")]
753        global: bool,
754        /// Write the project config file.
755        #[arg(long)]
756        project: bool,
757        /// Replace an existing config file.
758        #[arg(long)]
759        force: bool,
760    },
761    /// Get a resolved config value.
762    Get { key: String },
763    /// Set a config value.
764    Set {
765        /// Write the global config file.
766        #[arg(long, conflicts_with = "project")]
767        global: bool,
768        /// Write the project config file.
769        #[arg(long)]
770        project: bool,
771        /// Config key.
772        key: String,
773        /// Config value.
774        value: String,
775    },
776}
777
778/// TUI transcript render mode.
779#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
780#[serde(rename_all = "kebab-case")]
781pub enum TuiRenderMode {
782    /// Show assistant text, reasoning, tool calls, and tool returns.
783    #[default]
784    Normal,
785    /// Hide ordinary tool calls from transcript while keeping high-level events.
786    Concise,
787    /// Show detailed diagnostic rendering.
788    Debug,
789}
790
791/// Output mode.
792#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
793#[serde(rename_all = "kebab-case")]
794pub enum OutputMode {
795    /// Human-readable text.
796    Text,
797    /// Starweaver durable `DisplayMessage` JSON lines.
798    #[default]
799    DisplayJsonl,
800    /// Starweaver/AGUI top-level event JSON lines.
801    AguiJsonl,
802    /// Compact JSON command result.
803    Json,
804    /// Persist and print compact status.
805    Silent,
806}
807
808/// HITL policy.
809#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
810#[serde(rename_all = "snake_case")]
811pub enum HitlPolicy {
812    /// Deny approvals.
813    Deny,
814    /// Defer approvals.
815    Defer,
816    /// Fail on approvals.
817    Fail,
818    /// Prompt interactively.
819    #[default]
820    Prompt,
821}
822
823/// Build the clap command schema.
824#[must_use]
825pub fn command() -> clap::Command {
826    Cli::command()
827}
828
829/// Parse CLI arguments.
830pub fn parse(args: impl IntoIterator<Item = String>) -> CliResult<Cli> {
831    Cli::try_parse_from(args).map_err(|error| clap_error(&error))
832}
833
834/// Parse CLI arguments from OS strings.
835#[allow(dead_code)]
836pub fn parse_os(args: impl IntoIterator<Item = OsString>) -> CliResult<Cli> {
837    Cli::try_parse_from(args).map_err(|error| clap_error(&error))
838}
839
840fn clap_error(error: &clap::Error) -> CliError {
841    match error.kind() {
842        clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
843            CliError::Display(error.to_string())
844        }
845        _ => CliError::Usage(error.to_string()),
846    }
847}