返回 CodeWhale
runtime_options.rs
根目录 / crates / tui / src / runtime_options.rs
1 //! Startup facts passed by the canonical CLI to the Engine library.
2 //!
3 //! One Clap definition serves the CLI and the library's command decoder. Paths
4 //! remain native paths; no stringified argv round trip owns startup settings.
5
6 use anyhow::{Result, bail};
7 use clap::Args;
8 use std::path::PathBuf;
9
10 #[derive(Args, Debug, Default, Clone, PartialEq, Eq)]
11 pub struct RuntimeOptions {
12 /// Path to the config file to load instead of the default.
13 #[arg(long)]
14 pub config: Option<PathBuf>,
15 /// Config profile to apply (a `[profiles.<name>]` table).
16 #[arg(long)]
17 pub profile: Option<String>,
18 /// Workspace directory for Codewhale file tools (`-w` is a legacy alias).
19 #[arg(
20 short = 'C',
21 short_alias = 'w',
22 long = "workspace",
23 alias = "cd",
24 value_name = "DIR"
25 )]
26 pub workspace: Option<PathBuf>,
27 /// Enable terminal mouse capture for scrolling and transcript selection.
28 #[arg(long = "mouse-capture", conflicts_with = "no_mouse_capture")]
29 pub mouse_capture: bool,
30 /// Disable terminal mouse capture for terminal-native text selection.
31 #[arg(long = "no-mouse-capture", conflicts_with = "mouse_capture")]
32 pub no_mouse_capture: bool,
33 /// Skip onboarding screens.
34 #[arg(long)]
35 pub skip_onboarding: bool,
36 /// Start a fresh session without automatic resume or crash recovery.
37 #[arg(long)]
38 pub fresh: bool,
39 /// Skip project config and workspace overlays for this run.
40 #[arg(long)]
41 pub no_project_config: bool,
42 /// Enable a feature for this run (repeatable).
43 #[arg(long = "enable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
44 pub enable: Vec<String>,
45 /// Disable a feature for this run (repeatable).
46 #[arg(long = "disable", value_name = "FEATURE", action = clap::ArgAction::Append, global = true)]
47 pub disable: Vec<String>,
48 /// Legacy compatibility alias for Act + Full Access.
49 #[arg(long, hide = true)]
50 pub yolo: bool,
51 /// Maximum number of concurrent sub-agents (1-128; default 64).
52 #[arg(long)]
53 pub max_subagents: Option<usize>,
54 /// Enable verbose logging.
55 #[arg(short, long)]
56 pub verbose: bool,
57 /// Start account-owned web remote control for this interactive session.
58 #[arg(long, hide = true)]
59 pub remote_control: bool,
60 #[arg(skip)]
61 pub control_frontend: Option<codewhale_app_server::RuntimeControlFrontend>,
62 }
63
64 impl RuntimeOptions {
65 // A `run` passthrough can still carry startup flags. Preserve the old
66 // duplicate/conflict refusal rather than silently picking an authority.
67 pub(crate) fn merge(mut self, command: Self) -> Result<Self> {
68 macro_rules! scalar {
69 ($field:ident, $flag:literal) => {
70 if self.$field.is_some() && command.$field.is_some() {
71 bail!(
72 "{} cannot be supplied both before and inside the command",
73 $flag
74 );
75 }
76 self.$field = self.$field.or(command.$field);
77 };
78 }
79 macro_rules! flag {
80 ($field:ident, $name:literal) => {
81 if self.$field && command.$field {
82 bail!(
83 "{} cannot be supplied both before and inside the command",
84 $name
85 );
86 }
87 self.$field |= command.$field;
88 };
89 }
90 scalar!(config, "--config");
91 scalar!(profile, "--profile");
92 scalar!(workspace, "--workspace");
93 scalar!(max_subagents, "--max-subagents");
94 scalar!(control_frontend, "selected control frontend");
95 flag!(mouse_capture, "--mouse-capture");
96 flag!(no_mouse_capture, "--no-mouse-capture");
97 flag!(skip_onboarding, "--skip-onboarding");
98 flag!(fresh, "--fresh");
99 flag!(no_project_config, "--no-project-config");
100 flag!(yolo, "--yolo");
101 flag!(verbose, "--verbose");
102 flag!(remote_control, "--remote-control");
103 if self.mouse_capture && self.no_mouse_capture {
104 bail!("--mouse-capture conflicts with --no-mouse-capture");
105 }
106 self.enable.extend(command.enable);
107 self.disable.extend(command.disable);
108 Ok(self)
109 }
110
111 pub(crate) fn apply_features(&self, config: &mut crate::config::Config) -> Result<()> {
112 for feature in &self.enable {
113 config.set_feature(feature, true)?;
114 }
115 for feature in &self.disable {
116 config.set_feature(feature, false)?;
117 }
118 Ok(())
119 }
120 }
121
122 #[cfg(test)]
123 mod tests {
124 use super::*;
125
126 #[test]
127 fn typed_startup_rejects_competing_config_and_opposing_mouse_facts() {
128 let root = RuntimeOptions {
129 config: Some("root.toml".into()),
130 ..Default::default()
131 };
132 let command = RuntimeOptions {
133 config: Some("command.toml".into()),
134 ..Default::default()
135 };
136 assert!(
137 root.merge(command)
138 .unwrap_err()
139 .to_string()
140 .contains("--config")
141 );
142 let root = RuntimeOptions {
143 mouse_capture: true,
144 ..Default::default()
145 };
146 let command = RuntimeOptions {
147 no_mouse_capture: true,
148 ..Default::default()
149 };
150 assert!(
151 root.merge(command)
152 .unwrap_err()
153 .to_string()
154 .contains("conflicts")
155 );
156 }
157
158 #[test]
159 fn command_scope_facts_and_feature_disables_preserve_the_config_floor() {
160 let root = RuntimeOptions {
161 enable: vec!["web_search".into()],
162 ..Default::default()
163 };
164 let command = RuntimeOptions {
165 workspace: Some("command workspace".into()),
166 disable: vec!["web_search".into()],
167 no_project_config: true,
168 ..Default::default()
169 };
170 let merged = root.merge(command).unwrap();
171 assert_eq!(merged.workspace, Some(PathBuf::from("command workspace")));
172 assert!(merged.no_project_config);
173 let mut config = crate::config::Config::default();
174 merged.apply_features(&mut config).unwrap();
175 assert!(
176 !config
177 .features()
178 .enabled(crate::features::Feature::WebSearch)
179 );
180 }
181 }
182
182 lines RUST