Skip to main content

oxi/
cli.rs

1//! CLI argument parsing with clap
2//!
3//! Provides the unified command-line argument types for the oxi CLI.
4//! This is the single source of truth for all CLI parsing — main.rs
5//! imports from here rather than defining its own types.
6
7use clap::{Parser, Subcommand};
8use std::path::PathBuf;
9
10// ── Re-exports ─────────────────────────────────────────────────────
11// Use the canonical ThinkingLevel from settings (None/Minimal/Standard/Thorough).
12pub use oxi_store::settings::ThinkingLevel;
13
14// ── Main CLI arguments ─────────────────────────────────────────────
15
16/// CLI arguments
17#[derive(Debug, Clone, Parser)]
18#[command(name = "oxi")]
19#[command(about = "CLI coding harness for oxi")]
20#[command(version)]
21pub struct CliArgs {
22    /// pub.
23    #[command(subcommand)]
24    pub command: Option<Commands>,
25
26    /// Provider to use (e.g., anthropic, openai, google, deepseek)
27    #[arg(short, long)]
28    pub provider: Option<String>,
29
30    /// Model to use (e.g., claude-sonnet-4-20250514, gpt-4o)
31    #[arg(short, long)]
32    pub model: Option<String>,
33
34    /// Initial prompt (non-interactive mode)
35    #[arg(default_value = "")]
36    pub prompt: Vec<String>,
37
38    /// Interactive mode (default when no prompt is given)
39    #[arg(short, long)]
40    pub interactive: bool,
41
42    /// Thinking level (none, minimal, standard, thorough)
43    #[arg(long)]
44    pub thinking: Option<String>,
45
46    /// Load an extension from a shared library (.so / .dll / .dylib).
47    /// Can be specified multiple times.
48    #[arg(short = 'e', long = "extension", value_name = "PATH")]
49    pub extensions: Vec<PathBuf>,
50
51    /// Output mode: text or json (newline-delimited JSON events)
52    #[arg(long)]
53    pub mode: Option<String>,
54
55    /// Comma-separated list of tools to enable. Default: all builtins.
56    #[arg(long)]
57    pub tools: Option<String>,
58
59    /// Append system prompt from a file
60    #[arg(long)]
61    pub append_system_prompt: Option<PathBuf>,
62
63    /// Single-shot print mode (non-interactive)
64    #[arg(long)]
65    pub print: bool,
66
67    /// Disable session persistence
68    #[arg(long)]
69    pub no_session: bool,
70
71    /// Timeout in seconds for print mode
72    #[arg(long)]
73    pub timeout: Option<u64>,
74
75    /// Resume the most recent session for this project
76    #[arg(short, long)]
77    pub continue_session: bool,
78
79    // ── Routing configuration ─────────────────────────────────────────
80    /// Enable automatic model routing (falls back to cost-efficient models on errors)
81    #[arg(long = "enable-routing")]
82    pub enable_routing: bool,
83
84    /// Prefer cost-efficient models when routing is enabled
85    #[arg(long = "prefer-cost-efficient")]
86    pub prefer_cost_efficient: bool,
87
88    /// Fallback chain: comma-separated list of provider/model IDs (can be specified multiple times)
89    #[arg(long = "fallback-chain", value_delimiter = ',')]
90    pub fallback_chain: Vec<String>,
91
92    /// Disable automatic fallback (fail fast on errors instead of trying alternatives)
93    #[arg(long = "disable-fallback")]
94    pub disable_fallback: bool,
95}
96
97// ── Subcommands ────────────────────────────────────────────────────
98
99/// CLI subcommands
100#[derive(Debug, Clone, Subcommand)]
101pub enum Commands {
102    /// List all sessions.
103    Sessions,
104    /// Show session tree structure.
105    Tree {
106        /// Session ID to show tree for (default: current/last session)
107        #[arg(default_value = "")]
108        session_id: String,
109    },
110    /// Fork a new session from a specific entry
111    Fork {
112        /// Parent session ID
113        parent_id: String,
114        /// Entry ID to branch from
115        entry_id: String,
116    },
117    /// Delete a session
118    Delete {
119        /// Session ID to delete
120        session_id: String,
121    },
122    /// Package management
123    Pkg {
124        /// action.
125        #[command(subcommand)]
126        action: PkgCommands,
127    },
128    /// Configuration management
129    Config {
130        /// action.
131        #[command(subcommand)]
132        action: ConfigCommands,
133    },
134    /// Extension management — install, update, remove WASM extensions
135    Ext {
136        /// action.
137        #[command(subcommand)]
138        action: ExtCommands,
139    },
140    /// List available models
141    Models {
142        /// Filter by provider name (e.g., openai, anthropic, minimax)
143        #[arg(long)]
144        provider: Option<String>,
145    },
146    /// Run the interactive setup wizard
147    Setup {
148        /// Reset all settings to defaults
149        #[arg(long)]
150        reset: bool,
151    },
152    /// Reset all settings and data to factory defaults
153    ///
154    /// Use when configuration has become tangled and you want a clean start.
155    /// An interactive confirmation prompt will be shown.
156    Reset {
157        /// Skip the confirmation prompt
158        #[arg(long, short)]
159        yes: bool,
160        /// Also delete the project-local .oxi/ directory
161        #[arg(long)]
162        include_project: bool,
163    },
164    /// Export a session to HTML
165    Export {
166        /// Session ID to export (default: most recent for this project)
167        session_id: Option<String>,
168        /// Output file path (default: oxi-export-{id}.html in CWD)
169        #[arg(short, long)]
170        output: Option<PathBuf>,
171    },
172    /// Import a session from a JSONL file
173    Import {
174        /// Path to the JSONL session file
175        path: PathBuf,
176    },
177    /// Share a session as a GitHub Gist (requires gh CLI)
178    Share {
179        /// Session ID to share (default: most recent for this project)
180        session_id: Option<String>,
181    },
182}
183
184// ── Package subcommands ────────────────────────────────────────────
185
186/// Package management subcommands
187#[derive(Debug, Clone, Subcommand)]
188pub enum PkgCommands {
189    /// Install a package from a local path or npm:@scope/name
190    Install {
191        /// Package source: a local directory path or npm:@scope/name
192        source: String,
193    },
194    /// List installed packages
195    List,
196    /// Uninstall a package by name
197    Uninstall {
198        /// Package name to uninstall
199        name: String,
200    },
201    /// Update a package to the latest version
202    Update {
203        /// Package name to update (updates all if omitted)
204        name: Option<String>,
205    },
206}
207
208// ── Extension subcommands ──────────────────────────────────────────────
209
210/// Extension management subcommands
211#[derive(Debug, Clone, Subcommand)]
212pub enum ExtCommands {
213    /// Install a WASM extension from a GitHub repo (owner/repo or owner/repo@version)
214    Install {
215        /// Extension source: owner/repo or owner/repo@version
216        source: String,
217        /// Include pre-release versions
218        #[arg(long)]
219        prerelease: bool,
220    },
221    /// List installed extensions
222    List,
223    /// Remove an installed extension
224    Remove {
225        /// Extension name to remove
226        name: String,
227    },
228    /// Update extension(s) to latest version
229    Update {
230        /// Extension name to update (updates all if omitted)
231        name: Option<String>,
232    },
233    /// Show info about a remote extension (without installing)
234    Info {
235        /// Extension source: owner/repo
236        source: String,
237    },
238}
239
240// ── Config subcommands ─────────────────────────────────────────────
241
242/// Configuration management subcommands
243#[derive(Debug, Clone, Subcommand)]
244pub enum ConfigCommands {
245    /// Show current configuration
246    Show,
247    /// List all enabled resources
248    List {
249        /// Resource type filter (extensions, skills, prompts, themes)
250        resource_type: Option<String>,
251    },
252    /// Enable a resource (extension, skill, prompt, or theme)
253    Enable {
254        /// Resource type: extension, skill, prompt, or theme
255        resource_type: String,
256        /// Resource path or name
257        name: String,
258    },
259    /// Disable a resource
260    Disable {
261        /// Resource type: extension, skill, prompt, or theme
262        resource_type: String,
263        /// Resource path or name
264        name: String,
265    },
266    /// Set a configuration value
267    Set {
268        /// Setting key (e.g. theme, model, thinking_level)
269        key: String,
270        /// Setting value
271        value: String,
272    },
273    /// Get a configuration value
274    Get {
275        /// Setting key
276        key: String,
277    },
278    /// Add a custom OpenAI-compatible provider
279    AddProvider {
280        /// Provider name (e.g. minimax)
281        name: String,
282        /// Base URL (e.g. <https://api.minimax.chat/v1>)
283        base_url: String,
284        /// Environment variable name for API key (e.g. MINIMAX_API_KEY)
285        api_key_env: String,
286        /// API type: openai-completions or openai-responses (default: openai-completions)
287        #[arg(default_value = "openai-completions")]
288        api: String,
289    },
290    /// Remove a custom provider
291    RemoveProvider {
292        /// Provider name to remove
293        name: String,
294    },
295    /// Reset credentials (auth.json) and optionally settings
296    Reset {
297        /// Also reset settings (settings.toml / settings.json)
298        #[arg(long, short)]
299        all: bool,
300    },
301}
302
303// ── Parsing helpers ────────────────────────────────────────────────
304
305/// Parse CLI arguments from the command line
306///
307/// # Examples
308///
309/// ```ignore
310/// use oxi_cli::CliArgs;
311///
312/// fn main() {
313///     let args = CliArgs::parse();
314///     match args.command {
315///         Some(Commands::Sessions) => { /* list sessions */ }
316///         Some(Commands::Tree { session_id }) => { /* show tree */ }
317///         _ => { /* interactive mode */ }
318///     }
319/// }
320/// ```
321pub fn parse_args() -> CliArgs {
322    CliArgs::parse()
323}
324
325/// Parse CLI arguments from a specific iterator
326pub fn parse_args_from<I, T>(iter: I) -> Result<CliArgs, clap::Error>
327where
328    I: IntoIterator<Item = T>,
329    T: Into<std::ffi::OsString> + Clone,
330{
331    CliArgs::try_parse_from(iter)
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_parse_basic_prompt() {
340        let args = parse_args_from(["oxi", "Hello", "world"]).unwrap();
341        assert_eq!(args.prompt, vec!["Hello", "world"]);
342    }
343
344    #[test]
345    fn test_parse_with_provider_and_model() {
346        let args = parse_args_from([
347            "oxi",
348            "--provider",
349            "anthropic",
350            "--model",
351            "claude-sonnet-4-20250514",
352            "Hello",
353        ])
354        .unwrap();
355        assert_eq!(args.provider, Some("anthropic".to_string()));
356        assert_eq!(args.model, Some("claude-sonnet-4-20250514".to_string()));
357    }
358
359    #[test]
360    fn test_parse_interactive_flag() {
361        let args = parse_args_from(["oxi", "-i"]).unwrap();
362        assert!(args.interactive);
363    }
364
365    #[test]
366    fn test_parse_extension_paths() {
367        let args =
368            parse_args_from(["oxi", "-e", "/path/to/ext.so", "-e", "/other/ext.so"]).unwrap();
369        assert_eq!(args.extensions.len(), 2);
370    }
371
372    #[test]
373    fn test_parse_sessions_command() {
374        let args = parse_args_from(["oxi", "sessions"]).unwrap();
375        assert!(matches!(args.command, Some(Commands::Sessions)));
376    }
377
378    #[test]
379    fn test_parse_tree_command() {
380        let args = parse_args_from(["oxi", "tree", "abc-123"]).unwrap();
381        match args.command {
382            Some(Commands::Tree { session_id }) => {
383                assert_eq!(session_id, "abc-123");
384            }
385            _ => panic!("Expected Tree command"),
386        }
387    }
388
389    #[test]
390    fn test_parse_tree_command_default() {
391        let args = parse_args_from(["oxi", "tree"]).unwrap();
392        match args.command {
393            Some(Commands::Tree { session_id }) => {
394                assert_eq!(session_id, "");
395            }
396            _ => panic!("Expected Tree command"),
397        }
398    }
399
400    #[test]
401    fn test_parse_fork_command() {
402        let args = parse_args_from(["oxi", "fork", "parent-id", "entry-id"]).unwrap();
403        match args.command {
404            Some(Commands::Fork {
405                parent_id,
406                entry_id,
407            }) => {
408                assert_eq!(parent_id, "parent-id");
409                assert_eq!(entry_id, "entry-id");
410            }
411            _ => panic!("Expected Fork command"),
412        }
413    }
414
415    #[test]
416    fn test_parse_delete_command() {
417        let args = parse_args_from(["oxi", "delete", "session-123"]).unwrap();
418        match args.command {
419            Some(Commands::Delete { session_id }) => {
420                assert_eq!(session_id, "session-123");
421            }
422            _ => panic!("Expected Delete command"),
423        }
424    }
425
426    #[test]
427    fn test_parse_pkg_install() {
428        let args = parse_args_from(["oxi", "pkg", "install", "npm:@scope/name"]).unwrap();
429        match args.command {
430            Some(Commands::Pkg { action }) => match action {
431                PkgCommands::Install { source } => {
432                    assert_eq!(source, "npm:@scope/name");
433                }
434                _ => panic!("Expected Install subcommand"),
435            },
436            _ => panic!("Expected Pkg command"),
437        }
438    }
439
440    #[test]
441    fn test_parse_pkg_list() {
442        let args = parse_args_from(["oxi", "pkg", "list"]).unwrap();
443        match args.command {
444            Some(Commands::Pkg { action }) => {
445                assert!(matches!(action, PkgCommands::List));
446            }
447            _ => panic!("Expected Pkg command"),
448        }
449    }
450
451    #[test]
452    fn test_parse_pkg_update_all() {
453        let args = parse_args_from(["oxi", "pkg", "update"]).unwrap();
454        match args.command {
455            Some(Commands::Pkg { action }) => match action {
456                PkgCommands::Update { name } => assert!(name.is_none()),
457                _ => panic!("Expected Update subcommand"),
458            },
459            _ => panic!("Expected Pkg command"),
460        }
461    }
462
463    #[test]
464    fn test_parse_pkg_update_named() {
465        let args = parse_args_from(["oxi", "pkg", "update", "my-pkg"]).unwrap();
466        match args.command {
467            Some(Commands::Pkg { action }) => match action {
468                PkgCommands::Update { name } => assert_eq!(name, Some("my-pkg".to_string())),
469                _ => panic!("Expected Update subcommand"),
470            },
471            _ => panic!("Expected Pkg command"),
472        }
473    }
474
475    #[test]
476    fn test_parse_config_show() {
477        let args = parse_args_from(["oxi", "config", "show"]).unwrap();
478        assert!(matches!(
479            args.command,
480            Some(Commands::Config {
481                action: ConfigCommands::Show
482            })
483        ));
484    }
485
486    #[test]
487    fn test_parse_config_set() {
488        let args = parse_args_from(["oxi", "config", "set", "theme", "dracula"]).unwrap();
489        match args.command {
490            Some(Commands::Config { action }) => match action {
491                ConfigCommands::Set { key, value } => {
492                    assert_eq!(key, "theme");
493                    assert_eq!(value, "dracula");
494                }
495                _ => panic!("Expected Set subcommand"),
496            },
497            _ => panic!("Expected Config command"),
498        }
499    }
500
501    #[test]
502    fn test_parse_config_get() {
503        let args = parse_args_from(["oxi", "config", "get", "theme"]).unwrap();
504        match args.command {
505            Some(Commands::Config { action }) => match action {
506                ConfigCommands::Get { key } => {
507                    assert_eq!(key, "theme");
508                }
509                _ => panic!("Expected Get subcommand"),
510            },
511            _ => panic!("Expected Config command"),
512        }
513    }
514
515    #[test]
516    fn test_parse_config_enable() {
517        let args = parse_args_from(["oxi", "config", "enable", "extension", "my-ext"]).unwrap();
518        match args.command {
519            Some(Commands::Config { action }) => match action {
520                ConfigCommands::Enable {
521                    resource_type,
522                    name,
523                } => {
524                    assert_eq!(resource_type, "extension");
525                    assert_eq!(name, "my-ext");
526                }
527                _ => panic!("Expected Enable subcommand"),
528            },
529            _ => panic!("Expected Config command"),
530        }
531    }
532
533    #[test]
534    fn test_parse_config_disable() {
535        let args = parse_args_from(["oxi", "config", "disable", "skill", "my-skill"]).unwrap();
536        match args.command {
537            Some(Commands::Config { action }) => match action {
538                ConfigCommands::Disable {
539                    resource_type,
540                    name,
541                } => {
542                    assert_eq!(resource_type, "skill");
543                    assert_eq!(name, "my-skill");
544                }
545                _ => panic!("Expected Disable subcommand"),
546            },
547            _ => panic!("Expected Config command"),
548        }
549    }
550
551    #[test]
552    fn test_parse_config_list() {
553        let args = parse_args_from(["oxi", "config", "list"]).unwrap();
554        match args.command {
555            Some(Commands::Config { action }) => match action {
556                ConfigCommands::List { resource_type } => {
557                    assert!(resource_type.is_none());
558                }
559                _ => panic!("Expected List subcommand"),
560            },
561            _ => panic!("Expected Config command"),
562        }
563    }
564
565    #[test]
566    fn test_parse_config_list_filtered() {
567        let args = parse_args_from(["oxi", "config", "list", "extensions"]).unwrap();
568        match args.command {
569            Some(Commands::Config { action }) => match action {
570                ConfigCommands::List { resource_type } => {
571                    assert_eq!(resource_type, Some("extensions".to_string()));
572                }
573                _ => panic!("Expected List subcommand"),
574            },
575            _ => panic!("Expected Config command"),
576        }
577    }
578
579    #[test]
580    fn test_thinking_level_reexport() {
581        // Verify the re-export from settings works
582        assert_eq!(format!("{:?}", ThinkingLevel::Medium), "Medium");
583    }
584
585    #[test]
586    fn test_parse_config_add_provider() {
587        let args = parse_args_from([
588            "oxi",
589            "config",
590            "add-provider",
591            "minimax",
592            "https://api.minimax.chat/v1",
593            "MINIMAX_API_KEY",
594            "openai-completions",
595        ])
596        .unwrap();
597        match args.command {
598            Some(Commands::Config { action }) => match action {
599                ConfigCommands::AddProvider {
600                    name,
601                    base_url,
602                    api_key_env,
603                    api,
604                } => {
605                    assert_eq!(name, "minimax");
606                    assert_eq!(base_url, "https://api.minimax.chat/v1");
607                    assert_eq!(api_key_env, "MINIMAX_API_KEY");
608                    assert_eq!(api, "openai-completions");
609                }
610                _ => panic!("Expected AddProvider subcommand"),
611            },
612            _ => panic!("Expected Config command"),
613        }
614    }
615
616    #[test]
617    fn test_parse_config_add_provider_default_api() {
618        let args = parse_args_from([
619            "oxi",
620            "config",
621            "add-provider",
622            "zai",
623            "https://api.z.ai/v1",
624            "ZAI_API_KEY",
625        ])
626        .unwrap();
627        match args.command {
628            Some(Commands::Config { action }) => match action {
629                ConfigCommands::AddProvider {
630                    name,
631                    base_url,
632                    api_key_env,
633                    api,
634                } => {
635                    assert_eq!(name, "zai");
636                    assert_eq!(base_url, "https://api.z.ai/v1");
637                    assert_eq!(api_key_env, "ZAI_API_KEY");
638                    assert_eq!(api, "openai-completions"); // default
639                }
640                _ => panic!("Expected AddProvider subcommand"),
641            },
642            _ => panic!("Expected Config command"),
643        }
644    }
645
646    #[test]
647    fn test_parse_config_remove_provider() {
648        let args = parse_args_from(["oxi", "config", "remove-provider", "minimax"]).unwrap();
649        match args.command {
650            Some(Commands::Config { action }) => match action {
651                ConfigCommands::RemoveProvider { name } => {
652                    assert_eq!(name, "minimax");
653                }
654                _ => panic!("Expected RemoveProvider subcommand"),
655            },
656            _ => panic!("Expected Config command"),
657        }
658    }
659
660    #[test]
661    fn test_parse_models_command() {
662        let args = parse_args_from(["oxi", "models"]).unwrap();
663        match args.command {
664            Some(Commands::Models { provider }) => {
665                assert!(provider.is_none());
666            }
667            _ => panic!("Expected Models command"),
668        }
669    }
670
671    #[test]
672    fn test_parse_models_with_provider() {
673        let args = parse_args_from(["oxi", "models", "--provider", "minimax"]).unwrap();
674        match args.command {
675            Some(Commands::Models { provider }) => {
676                assert_eq!(provider, Some("minimax".to_string()));
677            }
678            _ => panic!("Expected Models command"),
679        }
680    }
681
682    #[test]
683    fn test_parse_setup_command() {
684        let args = parse_args_from(["oxi", "setup"]).unwrap();
685        match args.command {
686            Some(Commands::Setup { reset }) => {
687                assert!(!reset);
688            }
689            _ => panic!("Expected Setup command"),
690        }
691    }
692
693    #[test]
694    fn test_parse_setup_reset() {
695        let args = parse_args_from(["oxi", "setup", "--reset"]).unwrap();
696        match args.command {
697            Some(Commands::Setup { reset }) => {
698                assert!(reset);
699            }
700            _ => panic!("Expected Setup command with reset"),
701        }
702    }
703
704    // ── Routing flags ──────────────────────────────────────────────
705
706    #[test]
707    fn test_parse_enable_routing_flag() {
708        let args = parse_args_from(["oxi", "--enable-routing", "Hello"]).unwrap();
709        assert!(args.enable_routing);
710        assert!(!args.prefer_cost_efficient);
711        assert!(args.fallback_chain.is_empty());
712        assert!(!args.disable_fallback);
713    }
714
715    #[test]
716    fn test_parse_prefer_cost_efficient_flag() {
717        let args = parse_args_from(["oxi", "--prefer-cost-efficient", "Hello"]).unwrap();
718        // prefer_cost_efficient alone should NOT set enable_routing
719        assert!(!args.enable_routing); // enable_routing is a separate flag
720        assert!(args.prefer_cost_efficient);
721        assert!(args.fallback_chain.is_empty());
722        assert!(!args.disable_fallback);
723    }
724
725    #[test]
726    fn test_parse_fallback_chain_single() {
727        let args = parse_args_from(["oxi", "--fallback-chain", "openai/gpt-4o", "Hello"]).unwrap();
728        assert_eq!(args.fallback_chain, vec!["openai/gpt-4o"]);
729    }
730
731    #[test]
732    fn test_parse_fallback_chain_comma_separated() {
733        let args = parse_args_from([
734            "oxi",
735            "--fallback-chain",
736            "openai/gpt-4o,anthropic/claude-3",
737            "Hello",
738        ])
739        .unwrap();
740        assert_eq!(
741            args.fallback_chain,
742            vec!["openai/gpt-4o", "anthropic/claude-3"]
743        );
744    }
745
746    #[test]
747    fn test_parse_fallback_chain_multiple_args() {
748        let args = parse_args_from([
749            "oxi",
750            "--fallback-chain",
751            "openai/gpt-4o",
752            "--fallback-chain",
753            "anthropic/claude-3",
754            "Hello",
755        ])
756        .unwrap();
757        assert_eq!(
758            args.fallback_chain,
759            vec!["openai/gpt-4o", "anthropic/claude-3"]
760        );
761    }
762
763    #[test]
764    fn test_parse_fallback_chain_empty() {
765        let args = parse_args_from(["oxi", "Hello"]).unwrap();
766        assert!(args.fallback_chain.is_empty());
767    }
768
769    #[test]
770    fn test_parse_disable_fallback_flag() {
771        let args = parse_args_from(["oxi", "--disable-fallback", "Hello"]).unwrap();
772        assert!(args.disable_fallback);
773    }
774
775    #[test]
776    fn test_parse_routing_all_flags() {
777        let args = parse_args_from([
778            "oxi",
779            "--enable-routing",
780            "--prefer-cost-efficient",
781            "--fallback-chain",
782            "openai/gpt-4o,anthropic/claude-3",
783            "--disable-fallback",
784            "Hello",
785        ])
786        .unwrap();
787        assert!(args.enable_routing);
788        assert!(args.prefer_cost_efficient);
789        assert_eq!(
790            args.fallback_chain,
791            vec!["openai/gpt-4o", "anthropic/claude-3"]
792        );
793        assert!(args.disable_fallback);
794    }
795
796    // ── Reset command ────────────────────────────────────────────
797
798    #[test]
799    fn test_parse_reset_command() {
800        let args = parse_args_from(["oxi", "reset"]).unwrap();
801        match args.command {
802            Some(Commands::Reset {
803                yes,
804                include_project,
805            }) => {
806                assert!(!yes);
807                assert!(!include_project);
808            }
809            _ => panic!("Expected Reset command"),
810        }
811    }
812
813    #[test]
814    fn test_parse_reset_yes_flag() {
815        let args = parse_args_from(["oxi", "reset", "--yes"]).unwrap();
816        match args.command {
817            Some(Commands::Reset {
818                yes,
819                include_project,
820            }) => {
821                assert!(yes);
822                assert!(!include_project);
823            }
824            _ => panic!("Expected Reset command with --yes"),
825        }
826    }
827
828    #[test]
829    fn test_parse_reset_include_project() {
830        let args = parse_args_from(["oxi", "reset", "--yes", "--include-project"]).unwrap();
831        match args.command {
832            Some(Commands::Reset {
833                yes,
834                include_project,
835            }) => {
836                assert!(yes);
837                assert!(include_project);
838            }
839            _ => panic!("Expected Reset command with all flags"),
840        }
841    }
842}