返回 CodeWhale
setup.rs
根目录 / crates / tui / src / commands / groups / core / setup.rs
1 //! `/setup` command. `/setup fleet` opens the fleet readiness step.
2
3 use crate::commands::traits::{CommandInfo, RegisterCommand};
4 #[cfg(test)]
5 use crate::config::ApiProvider;
6 use crate::tui::app::{App, AppAction};
7 use codewhale_localization::MessageId;
8
9 use super::CommandResult;
10 use codewhale_config::SetupStep;
11
12 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
13 name: "setup",
14 aliases: &[],
15 usage: "/setup [fleet|provider|runtime|constitution|status|hotbar|tools|remote|persistence]",
16 description_id: MessageId::CmdSetupDescription,
17 };
18
19 pub(in crate::commands) struct SetupCmd;
20
21 impl RegisterCommand for SetupCmd {
22 fn info() -> &'static CommandInfo {
23 &COMMAND_INFO
24 }
25
26 fn execute(_app: &mut App, arg: Option<&str>) -> CommandResult {
27 let target = arg.map(str::trim).filter(|arg| !arg.is_empty());
28 if let Some(arg) = target {
29 let mut parts = arg.split_whitespace();
30 if matches!(parts.next(), Some("provider" | "providers"))
31 && let Some(raw_provider) = parts.next()
32 {
33 if parts.next().is_some() {
34 return CommandResult::error(
35 "Usage: /setup provider [provider-name]".to_string(),
36 );
37 }
38 return match super::provider::provider_setup_action_for_name(raw_provider) {
39 Ok(action) => CommandResult::action(action),
40 Err(message) => CommandResult::error(message),
41 };
42 }
43 }
44
45 match target {
46 None | Some("open" | "wizard" | "checkpoint") => {
47 CommandResult::action(AppAction::OpenSetupWizard)
48 }
49 Some("provider" | "providers" | "model" | "models" | "route") => {
50 CommandResult::action(AppAction::OpenSetupWizardAt {
51 step: SetupStep::ProviderModel,
52 })
53 }
54 Some("runtime" | "posture" | "trust" | "sandbox") => {
55 CommandResult::action(AppAction::OpenSetupWizardAt {
56 step: SetupStep::TrustSandbox,
57 })
58 }
59 Some("constitution" | "law") => CommandResult::action(AppAction::OpenSetupWizardAt {
60 step: SetupStep::Constitution,
61 }),
62 Some("status" | "report" | "verification" | "verify") => {
63 CommandResult::action(AppAction::OpenSetupWizardAt {
64 step: SetupStep::Verification,
65 })
66 }
67 Some("fleet" | "operate" | "operate-fleet" | "operate_fleet") => {
68 CommandResult::action(AppAction::OpenSetupWizardAt {
69 step: SetupStep::OperateFleet,
70 })
71 }
72 Some("hotbar" | "hotkeys" | "shortcuts" | "keys") => {
73 CommandResult::action(AppAction::OpenSetupWizardAt {
74 step: SetupStep::Hotbar,
75 })
76 }
77 Some("tools" | "tool" | "mcp" | "tools-mcp" | "tools_mcp" | "skills" | "plugins") => {
78 CommandResult::action(AppAction::OpenSetupWizardAt {
79 step: SetupStep::ToolsMcp,
80 })
81 }
82 Some(
83 "remote" | "remote-runtime" | "remote_runtime" | "cloud" | "bridge" | "mobile"
84 | "phone",
85 ) => CommandResult::action(AppAction::OpenSetupWizardAt {
86 step: SetupStep::RemoteRuntime,
87 }),
88 Some("persistence" | "persist" | "storage") => {
89 CommandResult::action(AppAction::OpenSetupWizardAt {
90 step: SetupStep::Persistence,
91 })
92 }
93 Some(other) => CommandResult::error(format!(
94 "Unknown /setup target '{other}'. Try `/setup fleet` to configure saved teams, or \
95 `/setup` to open the full setup wizard."
96 )),
97 }
98 }
99 }
100
101 #[cfg(test)]
102 mod tests {
103 use super::*;
104 use crate::config::Config;
105 use crate::tui::app::TuiOptions;
106 use std::path::PathBuf;
107
108 fn test_app() -> App {
109 let options = TuiOptions {
110 ..crate::test_support::test_tui_options(PathBuf::from("."))
111 };
112 App::new(options, &Config::default())
113 }
114
115 #[test]
116 fn setup_command_opens_wizard() {
117 let mut app = test_app();
118
119 let result = SetupCmd::execute(&mut app, None);
120
121 assert_eq!(result.action, Some(AppAction::OpenSetupWizard));
122 assert!(result.message.is_none());
123 }
124
125 #[test]
126 fn setup_checkpoint_alias_opens_wizard() {
127 let mut app = test_app();
128
129 let result = SetupCmd::execute(&mut app, Some("checkpoint"));
130
131 assert_eq!(result.action, Some(AppAction::OpenSetupWizard));
132 assert!(result.message.is_none());
133 }
134
135 #[test]
136 fn setup_report_opens_verification_step() {
137 let mut app = test_app();
138
139 let result = SetupCmd::execute(&mut app, Some("report"));
140
141 assert_eq!(
142 result.action,
143 Some(AppAction::OpenSetupWizardAt {
144 step: SetupStep::Verification
145 })
146 );
147 assert!(result.message.is_none());
148 }
149
150 #[test]
151 fn setup_named_steps_open_matching_wizard_cards() {
152 let cases = [
153 ("provider", SetupStep::ProviderModel),
154 ("model", SetupStep::ProviderModel),
155 ("runtime", SetupStep::TrustSandbox),
156 ("posture", SetupStep::TrustSandbox),
157 ("constitution", SetupStep::Constitution),
158 ("hotbar", SetupStep::Hotbar),
159 ("shortcuts", SetupStep::Hotbar),
160 ("tools", SetupStep::ToolsMcp),
161 ("tool", SetupStep::ToolsMcp),
162 ("tools-mcp", SetupStep::ToolsMcp),
163 ("tools_mcp", SetupStep::ToolsMcp),
164 ("mcp", SetupStep::ToolsMcp),
165 ("skills", SetupStep::ToolsMcp),
166 ("plugins", SetupStep::ToolsMcp),
167 ("remote", SetupStep::RemoteRuntime),
168 ("cloud", SetupStep::RemoteRuntime),
169 ("persistence", SetupStep::Persistence),
170 ("persist", SetupStep::Persistence),
171 ("storage", SetupStep::Persistence),
172 ];
173
174 for (arg, step) in cases {
175 let mut app = test_app();
176 let result = SetupCmd::execute(&mut app, Some(arg));
177 assert_eq!(
178 result.action,
179 Some(AppAction::OpenSetupWizardAt { step }),
180 "{arg}"
181 );
182 assert!(result.message.is_none(), "{arg}");
183 }
184 }
185
186 #[test]
187 fn setup_fleet_target_opens_the_operate_fleet_step() {
188 for target in ["fleet", "operate", "operate-fleet", "operate_fleet"] {
189 let mut app = test_app();
190 let result = SetupCmd::execute(&mut app, Some(target));
191
192 assert_eq!(
193 result.action,
194 Some(AppAction::OpenSetupWizardAt {
195 step: SetupStep::OperateFleet
196 }),
197 "{target}"
198 );
199 assert!(result.message.is_none(), "{target}");
200 }
201 }
202
203 #[test]
204 fn setup_retired_pod_target_is_rejected() {
205 let mut app = test_app();
206 let result = SetupCmd::execute(&mut app, Some("pod"));
207
208 assert!(result.is_error);
209 assert!(
210 result
211 .message
212 .as_deref()
213 .is_some_and(|message| message.contains("/setup fleet")),
214 "retired target must point at the canonical spelling, got: {result:?}"
215 );
216 }
217
218 #[test]
219 fn setup_usage_advertises_the_canonical_fleet_target() {
220 assert!(SetupCmd::info().usage.contains("fleet"));
221 assert!(!SetupCmd::info().usage.contains("pod"));
222 }
223
224 #[test]
225 fn setup_unknown_target_points_to_fleet_setup() {
226 let mut app = test_app();
227 let result = SetupCmd::execute(&mut app, Some("bogus"));
228
229 assert!(result.is_error);
230 assert!(
231 result
232 .message
233 .as_deref()
234 .is_some_and(|message| message.contains("/setup fleet"))
235 );
236 }
237
238 #[test]
239 fn setup_provider_named_opens_provider_setup_catalog() {
240 let mut app = test_app();
241
242 let result = SetupCmd::execute(&mut app, Some("provider anthropic"));
243
244 assert_eq!(
245 result.action,
246 Some(AppAction::OpenProviderSetup {
247 provider: Some(ApiProvider::Anthropic)
248 })
249 );
250 assert!(result.message.is_none());
251 }
252
253 #[test]
254 fn setup_provider_ds4_opens_keyless_local_preset() {
255 let mut app = test_app();
256
257 let result = SetupCmd::execute(&mut app, Some("provider ds4"));
258
259 assert_eq!(result.action, Some(AppAction::OpenDs4Setup));
260 assert!(result.message.is_none());
261 }
262
263 #[test]
264 fn setup_provider_agnes_rejects_retired_template_name() {
265 let mut app = test_app();
266
267 let result = SetupCmd::execute(&mut app, Some("provider agnes"));
268
269 assert!(result.action.is_none());
270 assert!(
271 result
272 .message
273 .as_deref()
274 .is_some_and(|message| message.contains("Unknown provider 'agnes'"))
275 );
276 }
277
278 #[test]
279 fn setup_provider_named_rejects_unknown_provider() {
280 let mut app = test_app();
281
282 let result = SetupCmd::execute(&mut app, Some("provider imaginary"));
283
284 assert!(result.action.is_none());
285 assert!(
286 result
287 .message
288 .as_deref()
289 .is_some_and(|message| message.contains("Unknown provider 'imaginary'"))
290 );
291 }
292 }
293
293 lines RUST