1use crate::commands::{Command, CommandExecutor, CommandResult, OutputFormat};
6use crate::config::Config;
7use crate::constants::{help, ui::emoji};
8use crate::errors::prelude::{CliError, Result as CliResult};
9use crate::output::{CliResponse, OutputFormatter};
10use async_trait::async_trait;
11use clap::Subcommand;
12use colored::Colorize;
13use serde_json::json;
14use std::io::{self, Write};
15use vkteams_bot::prelude::*;
16
17#[derive(Subcommand, Debug, Clone)]
19pub enum ConfigCommands {
20 Setup,
22 Examples,
24 ListCommands,
26 Validate,
28 Config {
30 #[arg(short, long)]
32 show: bool,
33 #[arg(short, long)]
35 init: bool,
36 #[arg(short = 'w', long)]
38 wizard: bool,
39 },
40 Completion {
42 #[arg(value_enum)]
44 shell: crate::completion::CompletionShell,
45 #[arg(short, long)]
47 output: Option<String>,
48 #[arg(short, long)]
50 install: bool,
51 #[arg(short, long)]
53 all: bool,
54 },
55}
56
57#[async_trait]
58impl Command for ConfigCommands {
59 async fn execute(&self, bot: &Bot) -> CliResult<()> {
60 match self {
61 ConfigCommands::Setup => execute_setup().await,
62 ConfigCommands::Examples => execute_examples().await,
63 ConfigCommands::ListCommands => execute_list_commands().await,
64 ConfigCommands::Validate => execute_validate(bot).await,
65 ConfigCommands::Config { show, init, wizard } => {
66 execute_config(*show, *init, *wizard).await
67 }
68 ConfigCommands::Completion {
69 shell,
70 output,
71 install,
72 all,
73 } => execute_completion(*shell, output.as_deref(), *install, *all).await,
74 }
75 }
76
77 fn name(&self) -> &'static str {
78 match self {
79 ConfigCommands::Setup => "setup",
80 ConfigCommands::Examples => "examples",
81 ConfigCommands::ListCommands => "list-commands",
82 ConfigCommands::Validate => "validate",
83 ConfigCommands::Config { .. } => "config",
84 ConfigCommands::Completion { .. } => "completion",
85 }
86 }
87
88 fn validate(&self) -> CliResult<()> {
89 Ok(())
91 }
92
93 async fn execute_with_output(&self, bot: &Bot, output_format: &OutputFormat) -> CliResult<()> {
95 let is_interactive = matches!(self, ConfigCommands::Setup)
97 || matches!(self, ConfigCommands::Config { wizard: true, .. });
98
99 if is_interactive && matches!(output_format, OutputFormat::Pretty) {
100 return self.execute(bot).await;
101 }
102
103 let response = match self {
104 ConfigCommands::Setup => execute_setup_structured().await,
105 ConfigCommands::Examples => execute_examples_structured().await,
106 ConfigCommands::ListCommands => execute_list_commands_structured().await,
107 ConfigCommands::Validate => execute_validate_structured(bot).await,
108 ConfigCommands::Config { show, init, wizard } => {
109 execute_config_structured(*show, *init, *wizard).await
110 }
111 ConfigCommands::Completion {
112 shell,
113 output,
114 install,
115 all,
116 } => execute_completion_structured(*shell, output.as_deref(), *install, *all).await,
117 };
118
119 OutputFormatter::print(&response, output_format)?;
120
121 if !response.success {
122 return Err(CliError::UnexpectedError("Command failed".to_string()));
123 }
124
125 Ok(())
126 }
127}
128
129#[async_trait]
130impl CommandExecutor for ConfigCommands {
131 async fn execute_with_result(&self, bot: &Bot) -> CommandResult {
132 match self {
133 ConfigCommands::Setup => execute_setup_with_result().await,
134 ConfigCommands::Examples => execute_examples_with_result().await,
135 ConfigCommands::ListCommands => execute_list_commands_with_result().await,
136 ConfigCommands::Validate => execute_validate_with_result(bot).await,
137 ConfigCommands::Config { show, init, wizard } => {
138 execute_config_with_result(*show, *init, *wizard).await
139 }
140 ConfigCommands::Completion {
141 shell,
142 output,
143 install,
144 all,
145 } => execute_completion_with_result(*shell, output.as_deref(), *install, *all).await,
146 }
147 }
148
149 fn name(&self) -> &'static str {
150 match self {
151 ConfigCommands::Setup => "setup",
152 ConfigCommands::Examples => "examples",
153 ConfigCommands::ListCommands => "list-commands",
154 ConfigCommands::Validate => "validate",
155 ConfigCommands::Config { .. } => "config",
156 ConfigCommands::Completion { .. } => "completion",
157 }
158 }
159
160 fn validate(&self) -> CliResult<()> {
161 Ok(())
163 }
164}
165
166async fn execute_setup() -> CliResult<()> {
169 println!(
170 "{} VK Teams Bot CLI Setup Wizard",
171 emoji::ROBOT.bold().blue()
172 );
173 println!("This wizard will help you configure the CLI tool.\n");
174
175 let mut new_config = toml::from_str::<Config>("").unwrap();
176
177 print!("Enter your VK Teams Bot API token: ");
179 io::stdout().flush().unwrap();
180 let mut token = String::new();
181 io::stdin().read_line(&mut token).unwrap();
182 new_config.api.token = Some(token.trim().to_string());
183
184 print!("Enter your VK Teams Bot API URL: ");
186 io::stdout().flush().unwrap();
187 let mut url = String::new();
188 io::stdin().read_line(&mut url).unwrap();
189 new_config.api.url = Some(url.trim().to_string());
190
191 print!("Do you need to configure a proxy? (y/N): ");
193 io::stdout().flush().unwrap();
194 let mut proxy_choice = String::new();
195 io::stdin().read_line(&mut proxy_choice).unwrap();
196 if proxy_choice.trim().to_lowercase() == "y" {
197 print!("Enter proxy URL: ");
198 io::stdout().flush().unwrap();
199 let mut proxy_url = String::new();
200 io::stdin().read_line(&mut proxy_url).unwrap();
201 new_config.proxy = Some(crate::config::ProxyConfig {
202 url: proxy_url.trim().to_string(),
203 user: None,
204 password: None,
205 });
206 }
207
208 println!("\n{} Testing configuration...", emoji::TEST_TUBE);
210
211 if let Err(e) = new_config.save(None) {
212 eprintln!(
213 "{} Warning: Could not save configuration: {}",
214 emoji::WARNING,
215 e
216 );
217 } else {
218 println!("{} Configuration saved successfully!", emoji::FLOPPY_DISK);
219 }
220
221 println!(
222 "\n{} Setup complete! You can now use the CLI tool.",
223 emoji::PARTY
224 );
225 println!(
226 "Try: {} to test your setup",
227 "vkteams-bot-cli get-self".green()
228 );
229
230 Ok(())
231}
232
233async fn execute_completion(
234 shell: crate::completion::CompletionShell,
235 output: Option<&str>,
236 install: bool,
237 all: bool,
238) -> CliResult<()> {
239 use crate::completion::{
240 generate_all_completions, generate_completion, get_default_completion_dir,
241 install_completion,
242 };
243 use std::path::Path;
244
245 if all {
246 let output_dir = if let Some(dir) = output {
247 Path::new(dir).to_path_buf()
248 } else if let Some(default_dir) = get_default_completion_dir() {
249 default_dir
250 } else {
251 std::env::current_dir()
252 .map_err(|e| CliError::FileError(format!("Failed to get current directory: {e}")))?
253 };
254
255 generate_all_completions(&output_dir)?;
256 return Ok(());
257 }
258
259 if install {
260 install_completion(shell)?;
261 return Ok(());
262 }
263
264 let output_path = output.map(Path::new);
265 generate_completion(shell, output_path)?;
266
267 Ok(())
268}
269
270async fn execute_setup_with_result() -> CommandResult {
273 match execute_setup().await {
274 Ok(()) => CommandResult::success_with_message("Setup completed successfully"),
275 Err(e) => CommandResult::error(format!("Setup failed: {e}")),
276 }
277}
278
279async fn execute_examples_with_result() -> CommandResult {
280 match execute_examples().await {
281 Ok(()) => CommandResult::success(),
282 Err(e) => CommandResult::error(format!("Failed to show examples: {e}")),
283 }
284}
285
286async fn execute_list_commands_with_result() -> CommandResult {
287 match execute_list_commands().await {
288 Ok(()) => CommandResult::success(),
289 Err(e) => CommandResult::error(format!("Failed to list commands: {e}")),
290 }
291}
292
293async fn execute_validate_with_result(bot: &Bot) -> CommandResult {
294 match execute_validate(bot).await {
295 Ok(()) => CommandResult::success_with_message("Validation completed successfully"),
296 Err(e) => CommandResult::error(format!("Validation failed: {e}")),
297 }
298}
299
300async fn execute_config_with_result(show: bool, init: bool, wizard: bool) -> CommandResult {
301 match execute_config(show, init, wizard).await {
302 Ok(()) => CommandResult::success_with_message("Configuration operation completed"),
303 Err(e) => CommandResult::error(format!("Configuration operation failed: {e}")),
304 }
305}
306
307async fn execute_completion_with_result(
308 shell: crate::completion::CompletionShell,
309 output: Option<&str>,
310 install: bool,
311 all: bool,
312) -> CommandResult {
313 match execute_completion(shell, output, install, all).await {
314 Ok(()) => CommandResult::success_with_message("Completion operation completed"),
315 Err(e) => CommandResult::error(format!("Completion operation failed: {e}")),
316 }
317}
318
319async fn execute_examples() -> CliResult<()> {
320 println!("{} VK Teams Bot CLI Examples", emoji::BOOKS.bold().blue());
321 println!();
322
323 println!("{}", "Basic Message Operations:".bold().green());
324 println!(
325 " {}",
326 "vkteams-bot-cli send-text -u USER_ID -m \"Hello World!\"".cyan()
327 );
328 println!(
329 " {}",
330 "vkteams-bot-cli send-file -u USER_ID -p /path/to/file.pdf".cyan()
331 );
332 println!(
333 " {}",
334 "vkteams-bot-cli send-voice -u USER_ID -p /path/to/voice.ogg".cyan()
335 );
336 println!();
337
338 println!("{}", "Chat Management:".bold().green());
339 println!(" {}", "vkteams-bot-cli get-chat-info -c CHAT_ID".cyan());
340 println!(" {}", "vkteams-bot-cli get-chat-members -c CHAT_ID".cyan());
341 println!(
342 " {}",
343 "vkteams-bot-cli set-chat-title -c CHAT_ID -t \"New Title\"".cyan()
344 );
345 println!(
346 " {}",
347 "vkteams-bot-cli set-chat-about -c CHAT_ID -a \"Chat description\"".cyan()
348 );
349 println!();
350
351 println!("{}", "Message Operations:".bold().green());
352 println!(
353 " {}",
354 "vkteams-bot-cli edit-message -c CHAT_ID -m MSG_ID -t \"Updated text\"".cyan()
355 );
356 println!(
357 " {}",
358 "vkteams-bot-cli delete-message -c CHAT_ID -m MSG_ID".cyan()
359 );
360 println!(
361 " {}",
362 "vkteams-bot-cli pin-message -c CHAT_ID -m MSG_ID".cyan()
363 );
364 println!(
365 " {}",
366 "vkteams-bot-cli unpin-message -c CHAT_ID -m MSG_ID".cyan()
367 );
368 println!();
369
370 println!("{}", "File Operations:".bold().green());
371 println!(
372 " {}",
373 "vkteams-bot-cli get-file -f FILE_ID -p /download/path/".cyan()
374 );
375 println!();
376
377 println!("{}", "Bot Information:".bold().green());
378 println!(" {}", "vkteams-bot-cli get-self".cyan());
379 println!(" {}", "vkteams-bot-cli get-self --detailed".cyan());
380 println!(" {}", "vkteams-bot-cli get-profile -u USER_ID".cyan());
381 println!();
382
383 println!("{}", "Event Monitoring:".bold().green());
384 println!(" {}", "vkteams-bot-cli get-events".cyan());
385 println!(
386 " {}",
387 "vkteams-bot-cli get-events -l true | jq '.events[]'".cyan()
388 );
389 println!();
390
391 println!("{}", "Configuration:".bold().green());
392 println!(" {}", "vkteams-bot-cli setup".cyan());
393 println!(" {}", "vkteams-bot-cli config --show".cyan());
394 println!(" {}", "vkteams-bot-cli config --wizard".cyan());
395 println!(" {}", "vkteams-bot-cli validate".cyan());
396 println!();
397
398 println!("{}", "Shell Completion:".bold().green());
399 println!(" {}", "vkteams-bot-cli completion bash".cyan());
400 println!(
401 " {}",
402 "vkteams-bot-cli completion zsh --output _vkteams-bot-cli".cyan()
403 );
404 println!(" {}", "vkteams-bot-cli completion fish --install".cyan());
405 println!(
406 " {}",
407 "vkteams-bot-cli completion bash --all --output ./completions".cyan()
408 );
409 println!();
410
411 println!("{}", "Scheduled Messages:".bold().green());
412 println!(
413 " {}",
414 "vkteams-bot-cli schedule text -u CHAT_ID -m \"Hello\" -t \"2024-01-01 10:00\"".cyan()
415 );
416 println!(
417 " {}",
418 "vkteams-bot-cli schedule text -u CHAT_ID -m \"Daily reminder\" -c \"0 9 * * *\"".cyan()
419 );
420 println!(
421 " {}",
422 "vkteams-bot-cli schedule text -u CHAT_ID -m \"Every 5 min\" -i 300".cyan()
423 );
424 println!(
425 " {}",
426 "vkteams-bot-cli schedule file -u CHAT_ID -p \"/path/to/report.pdf\" -t \"30m\"".cyan()
427 );
428 println!();
429
430 println!("{}", "Scheduler Management:".bold().green());
431 println!(" {}", "vkteams-bot-cli scheduler start".cyan());
432 println!(" {}", "vkteams-bot-cli scheduler status".cyan());
433 println!(" {}", "vkteams-bot-cli scheduler list".cyan());
434 println!(" {}", "vkteams-bot-cli task show TASK_ID".cyan());
435 println!(" {}", "vkteams-bot-cli task run TASK_ID".cyan());
436 println!();
437
438 println!("{}", "Chat Actions:".bold().green());
439 println!(
440 " {}",
441 "vkteams-bot-cli send-action -c CHAT_ID -a typing".cyan()
442 );
443 println!(
444 " {}",
445 "vkteams-bot-cli send-action -c CHAT_ID -a looking".cyan()
446 );
447 println!();
448
449 Ok(())
450}
451
452async fn execute_list_commands() -> CliResult<()> {
453 println!(
454 "{} VK Teams Bot CLI Commands Reference",
455 emoji::ROBOT.bold().blue()
456 );
457 println!();
458
459 let commands = vec![
460 (
461 "send-text",
462 "Send a text message to a user or chat",
463 "Basic messaging",
464 ),
465 (
466 "send-file",
467 "Upload and send a file to a user or chat",
468 "File sharing",
469 ),
470 (
471 "send-voice",
472 "Send a voice message from an audio file",
473 "Voice messaging",
474 ),
475 (
476 "get-file",
477 "Download a file by its ID to local storage",
478 "File management",
479 ),
480 (
481 "get-events",
482 "Retrieve bot events or start long polling",
483 "Event monitoring",
484 ),
485 (
486 "get-chat-info",
487 "Get detailed information about a chat",
488 "Chat information",
489 ),
490 (
491 "get-profile",
492 "Get user profile information",
493 "User information",
494 ),
495 (
496 "edit-message",
497 "Edit an existing message in a chat",
498 "Message management",
499 ),
500 (
501 "delete-message",
502 "Delete a message from a chat",
503 "Message management",
504 ),
505 (
506 "pin-message",
507 "Pin a message in a chat",
508 "Message management",
509 ),
510 (
511 "unpin-message",
512 "Unpin a message from a chat",
513 "Message management",
514 ),
515 (
516 "get-chat-members",
517 "List all members of a chat",
518 "Chat management",
519 ),
520 (
521 "set-chat-title",
522 "Change the title of a chat",
523 "Chat management",
524 ),
525 (
526 "set-chat-about",
527 "Set the description of a chat",
528 "Chat management",
529 ),
530 (
531 "send-action",
532 "Send typing or looking action to a chat",
533 "Chat interaction",
534 ),
535 (
536 "get-self",
537 "Get bot information and verify connectivity",
538 "Bot management",
539 ),
540 (
541 "schedule",
542 "Schedule messages to be sent at specific times",
543 "Scheduling",
544 ),
545 (
546 "scheduler",
547 "Manage the scheduler daemon service",
548 "Scheduling",
549 ),
550 ("task", "Manage individual scheduled tasks", "Scheduling"),
551 (
552 "setup",
553 "Interactive wizard for first-time configuration",
554 "Configuration",
555 ),
556 ("examples", "Show usage examples for all commands", "Help"),
557 (
558 "list-commands",
559 "Show this detailed command reference",
560 "Help",
561 ),
562 (
563 "validate",
564 "Test configuration and bot connectivity",
565 "Diagnostics",
566 ),
567 (
568 "config",
569 "Manage configuration files and settings",
570 "Configuration",
571 ),
572 (
573 "completion",
574 "Generate shell completion scripts",
575 "Configuration",
576 ),
577 ];
578
579 let mut categories: std::collections::HashMap<&str, Vec<(&str, &str)>> =
580 std::collections::HashMap::new();
581
582 for (cmd, desc, cat) in commands {
583 categories.entry(cat).or_default().push((cmd, desc));
584 }
585
586 for (category, cmds) in categories {
587 println!("{}", format!("{category}:").bold().green());
588 for (cmd, desc) in cmds {
589 println!(" {:<20} {}", cmd.cyan(), desc);
590 }
591 println!();
592 }
593
594 println!("{}", "💡 Tips:".bold().yellow());
595 println!(
596 " • Use {} for command-specific help",
597 "vkteams-bot-cli <command> --help".cyan()
598 );
599 println!(
600 " • Use {} to see usage examples",
601 "vkteams-bot-cli examples".cyan()
602 );
603 println!(
604 " • Use {} to test your configuration",
605 "vkteams-bot-cli validate".cyan()
606 );
607 println!(
608 " • Use {} for interactive setup",
609 "vkteams-bot-cli setup".cyan()
610 );
611
612 Ok(())
613}
614
615async fn execute_validate(bot: &Bot) -> CliResult<()> {
616 println!(
617 "{} Validating Configuration...",
618 emoji::MAGNIFYING_GLASS.bold().blue()
619 );
620 println!();
621
622 match Config::from_file() {
624 Ok(config) => {
625 println!("{} Configuration file found and readable", emoji::CHECK);
626
627 if config.api.token.is_some() {
629 println!("{} API token is configured", emoji::CHECK);
630 } else {
631 println!("{} API token is missing", emoji::CROSS);
632 }
633
634 if config.api.url.is_some() {
635 println!("{} API URL is configured", emoji::CHECK);
636 } else {
637 println!("{} API URL is missing", emoji::CROSS);
638 }
639
640 println!("\n{} Testing bot connection...", emoji::GEAR);
642
643 let request = RequestSelfGet::new(());
644 match bot.send_api_request(request).await {
645 Ok(bot_info) => {
646 println!("{} API connection successful", emoji::CHECK);
647 println!("{} Bot is working correctly", emoji::CHECK);
648
649 if let Ok(json_str) = serde_json::to_string_pretty(&bot_info) {
650 println!("\n{}", "Bot Information:".bold().green());
651 println!("{}", json_str.green());
652 }
653 }
654 Err(e) => {
655 println!("{} API connection failed: {}", emoji::CROSS, e);
656 return Err(CliError::ApiError(e));
657 }
658 }
659 }
660 Err(_) => {
661 println!("{} No configuration file found", emoji::CROSS);
662 println!(
663 "{} Run {} to create initial configuration",
664 emoji::LIGHTBULB,
665 "vkteams-bot-cli setup".cyan()
666 );
667 }
668 }
669
670 println!("\n{} Validation complete!", emoji::SPARKLES.bold().green());
671 Ok(())
672}
673
674async fn execute_config(show: bool, init: bool, wizard: bool) -> CliResult<()> {
675 if wizard {
676 println!("{} Configuration Wizard", emoji::GEAR.bold().blue());
677 println!("Current configuration will be updated.\n");
678
679 let mut new_config = toml::from_str::<Config>("").unwrap();
680
681 if let Ok(current_config) = Config::from_file()
683 && let Some(current_token) = ¤t_config.api.token
684 {
685 println!(
686 "Current API token: {}***",
687 ¤t_token[..8.min(current_token.len())]
688 );
689 }
690 print!("Enter new API token (or press Enter to keep current): ");
691 io::stdout().flush().unwrap();
692 let mut token = String::new();
693 io::stdin().read_line(&mut token).unwrap();
694 if !token.trim().is_empty() {
695 new_config.api.token = Some(token.trim().to_string());
696 }
697
698 if let Ok(current_config) = Config::from_file()
700 && let Some(current_url) = ¤t_config.api.url
701 {
702 println!("Current API URL: {current_url}");
703 }
704 print!("Enter new API URL (or press Enter to keep current): ");
705 io::stdout().flush().unwrap();
706 let mut url = String::new();
707 io::stdin().read_line(&mut url).unwrap();
708 if !url.trim().is_empty() {
709 new_config.api.url = Some(url.trim().to_string());
710 }
711
712 if let Err(e) = new_config.save(None) {
714 eprintln!(
715 "{} Warning: Could not save configuration: {}",
716 emoji::WARNING,
717 e
718 );
719 } else {
720 println!("{} Configuration updated successfully!", emoji::FLOPPY_DISK);
721 }
722 }
723
724 if show {
725 match Config::from_file() {
727 Ok(config) => match toml::to_string_pretty(&config) {
728 Ok(config_str) => {
729 println!("Current configuration:\n{}", config_str.green());
730 }
731 Err(e) => {
732 return Err(CliError::UnexpectedError(format!(
733 "Failed to serialize configuration: {e}"
734 )));
735 }
736 },
737 Err(_) => {
738 println!("{} No configuration file found", emoji::INFO);
739 println!("{} {}", emoji::LIGHTBULB, help::SETUP_HINT.cyan());
740 }
741 }
742 }
743
744 if init {
745 let config = toml::from_str::<Config>("").unwrap();
747 config.save(None)?;
748 println!("Configuration file initialized.");
749 }
750
751 if !show && !init && !wizard {
753 println!(
754 "Use --show to display current configuration, --init to create a new configuration file, or --wizard for interactive configuration."
755 );
756 }
757
758 Ok(())
759}
760
761async fn execute_setup_structured() -> CliResponse<serde_json::Value> {
764 CliResponse::success(
766 "setup",
767 json!({
768 "status": "interactive",
769 "message": "Setup wizard requires interactive terminal. Use regular execute mode.",
770 "help": "Run without --output-format flag for interactive setup"
771 }),
772 )
773}
774
775async fn execute_examples_structured() -> CliResponse<serde_json::Value> {
776 let examples = vec![
777 json!({
778 "category": "Messaging",
779 "examples": [
780 {
781 "command": "vkteams-bot-cli send-text -u @user -m \"Hello!\"",
782 "description": "Send text message to user"
783 },
784 {
785 "command": "vkteams-bot-cli send-file -u chatId -p /path/to/file.pdf",
786 "description": "Send file to chat"
787 }
788 ]
789 }),
790 json!({
791 "category": "Chat Management",
792 "examples": [
793 {
794 "command": "vkteams-bot-cli get-chat-info -c chatId",
795 "description": "Get chat information"
796 },
797 {
798 "command": "vkteams-bot-cli set-chat-title -c chatId -t \"New Title\"",
799 "description": "Set chat title"
800 }
801 ]
802 }),
803 json!({
804 "category": "Storage",
805 "examples": [
806 {
807 "command": "vkteams-bot-cli storage database init",
808 "description": "Initialize database with migrations"
809 },
810 {
811 "command": "vkteams-bot-cli storage search semantic -q \"search query\"",
812 "description": "Search using semantic similarity"
813 }
814 ]
815 }),
816 ];
817
818 CliResponse::success(
819 "examples",
820 json!({
821 "examples": examples,
822 "note": "Use --help with any command for detailed options"
823 }),
824 )
825}
826
827async fn execute_list_commands_structured() -> CliResponse<serde_json::Value> {
828 let commands = vec![
829 json!({
830 "category": "Messaging",
831 "commands": ["send-text", "send-file", "send-voice", "edit-message", "delete-message", "pin-message", "unpin-message"]
832 }),
833 json!({
834 "category": "Chat",
835 "commands": ["get-chat-info", "get-profile", "get-chat-members", "set-chat-title", "set-chat-about", "send-action"]
836 }),
837 json!({
838 "category": "Files",
839 "commands": ["upload", "download", "get-info"]
840 }),
841 json!({
842 "category": "Storage",
843 "commands": ["database", "search", "context"]
844 }),
845 json!({
846 "category": "Diagnostic",
847 "commands": ["get-self", "get-events", "get-file", "health-check", "network-test", "system-info", "rate-limit-test"]
848 }),
849 json!({
850 "category": "Config",
851 "commands": ["setup", "examples", "list-commands", "validate", "config", "completion"]
852 }),
853 json!({
854 "category": "Daemon",
855 "commands": ["start", "stop", "status", "restart", "logs"]
856 }),
857 ];
858
859 CliResponse::success(
860 "list-commands",
861 json!({
862 "command_categories": commands,
863 "total_categories": commands.len()
864 }),
865 )
866}
867
868async fn execute_validate_structured(bot: &Bot) -> CliResponse<serde_json::Value> {
869 let mut validation_results = Vec::new();
870
871 let config_result = match Config::from_file() {
873 Ok(config) => {
874 let mut issues = Vec::new();
875 if config.api.token.is_none() {
876 issues.push("Missing API token");
877 }
878 if config.api.url.is_none() {
879 issues.push("Missing API URL");
880 }
881
882 json!({
883 "test": "Configuration File",
884 "status": if issues.is_empty() { "pass" } else { "fail" },
885 "issues": issues
886 })
887 }
888 Err(e) => json!({
889 "test": "Configuration File",
890 "status": "fail",
891 "error": e.to_string()
892 }),
893 };
894 validation_results.push(config_result);
895
896 let connection_result = match bot.send_api_request(RequestSelfGet::new(())).await {
898 Ok(result) => json!({
899 "test": "Bot Connection",
900 "status": "pass",
901 "bot_info": {
902 "user_id": result.user_id,
903 "nickname": result.nick,
904 "first_name": result.first_name
905 }
906 }),
907 Err(e) => json!({
908 "test": "Bot Connection",
909 "status": "fail",
910 "error": e.to_string()
911 }),
912 };
913 validation_results.push(connection_result);
914
915 let env_vars = [
917 "VKTEAMS_BOT_API_TOKEN",
918 "VKTEAMS_BOT_API_URL",
919 "VKTEAMS_BOT_DOWNLOAD_DIR",
920 ];
921 let mut env_status = Vec::new();
922 for var in &env_vars {
923 env_status.push(json!({
924 "variable": var,
925 "set": std::env::var(var).is_ok()
926 }));
927 }
928 validation_results.push(json!({
929 "test": "Environment Variables",
930 "status": "info",
931 "variables": env_status
932 }));
933
934 let all_passed = validation_results
935 .iter()
936 .all(|r| r.get("status").and_then(|s| s.as_str()) != Some("fail"));
937
938 CliResponse::success(
939 "validate",
940 json!({
941 "validation_status": if all_passed { "valid" } else { "invalid" },
942 "test_results": validation_results
943 }),
944 )
945}
946
947async fn execute_config_structured(
948 show: bool,
949 init: bool,
950 wizard: bool,
951) -> CliResponse<serde_json::Value> {
952 if wizard {
953 return CliResponse::success(
954 "config",
955 json!({
956 "mode": "wizard",
957 "status": "interactive",
958 "message": "Configuration wizard requires interactive terminal",
959 "help": "Run without --output-format flag for interactive wizard"
960 }),
961 );
962 }
963
964 if init {
965 match Config::default().save(None) {
966 Ok(_) => {
967 let config_path = crate::utils::config_helpers::get_config_paths()
968 .first()
969 .map(|p| p.display().to_string())
970 .unwrap_or_else(|| "unknown".to_string());
971
972 CliResponse::success(
973 "config",
974 json!({
975 "action": "init",
976 "status": "created",
977 "path": config_path
978 }),
979 )
980 }
981 Err(e) => CliResponse::error("config", format!("Failed to initialize config: {e}")),
982 }
983 } else if show {
984 match Config::from_file() {
985 Ok(config) => {
986 let config_json = serde_json::to_value(&config).unwrap_or(json!({}));
987 CliResponse::success(
988 "config",
989 json!({
990 "action": "show",
991 "configuration": config_json
992 }),
993 )
994 }
995 Err(e) => CliResponse::error("config", format!("Failed to load config: {e}")),
996 }
997 } else {
998 CliResponse::success(
999 "config",
1000 json!({
1001 "message": "No action specified",
1002 "available_flags": ["--show", "--init", "--wizard"]
1003 }),
1004 )
1005 }
1006}
1007
1008async fn execute_completion_structured(
1009 shell: crate::completion::CompletionShell,
1010 output: Option<&str>,
1011 install: bool,
1012 all: bool,
1013) -> CliResponse<serde_json::Value> {
1014 if all {
1015 return CliResponse::success(
1016 "completion",
1017 json!({
1018 "mode": "all",
1019 "message": "Generating completions for all shells",
1020 "shells": ["bash", "zsh", "fish", "powershell"],
1021 "note": "Use regular execute mode to generate files"
1022 }),
1023 );
1024 }
1025
1026 let shell_name = match shell {
1027 crate::completion::CompletionShell::Bash => "bash",
1028 crate::completion::CompletionShell::Zsh => "zsh",
1029 crate::completion::CompletionShell::Fish => "fish",
1030 crate::completion::CompletionShell::PowerShell => "powershell",
1031 };
1032
1033 CliResponse::success(
1034 "completion",
1035 json!({
1036 "shell": shell_name,
1037 "output": output.unwrap_or("stdout"),
1038 "install": install,
1039 "note": "Completion script generation requires regular execute mode"
1040 }),
1041 )
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047 use crate::utils::config_helpers::get_existing_config_path;
1048 use std::fs;
1049 #[cfg(unix)]
1050 use std::os::unix::fs::PermissionsExt;
1051 use tempfile::tempdir;
1052
1053 fn dummy_bot() -> Bot {
1055 Bot::with_params(&APIVersionUrl::V1, "dummy_token", "https://dummy.api.com").unwrap()
1056 }
1057
1058 #[tokio::test]
1059 async fn test_config_commands_variants() {
1060 let _ = ConfigCommands::Setup;
1062 let _ = ConfigCommands::Examples;
1063 let _ = ConfigCommands::ListCommands;
1064 let _ = ConfigCommands::Validate;
1065 let _ = ConfigCommands::Config {
1066 show: true,
1067 init: false,
1068 wizard: false,
1069 };
1070 let _ = ConfigCommands::Completion {
1071 shell: crate::completion::CompletionShell::Bash,
1072 output: None,
1073 install: false,
1074 all: false,
1075 };
1076 }
1077
1078 #[tokio::test]
1079 async fn test_execute_examples_success() {
1080 let res = execute_examples().await;
1082 assert!(res.is_ok());
1083 }
1084
1085 #[tokio::test]
1086 async fn test_execute_list_commands_success() {
1087 let res = execute_list_commands().await;
1089 assert!(res.is_ok());
1090 }
1091
1092 #[tokio::test]
1093 async fn test_execute_config_show_success() {
1094 let res = execute_config(true, false, false).await;
1096 assert!(res.is_ok());
1097 }
1098
1099 #[tokio::test]
1100 async fn test_execute_config_init_success() {
1101 let res = execute_config(false, true, false).await;
1103 assert!(res.is_ok());
1104 }
1105
1106 #[tokio::test]
1107 #[ignore]
1108 async fn test_execute_config_wizard_success() {
1109 let res = execute_config(false, false, true).await;
1111 assert!(res.is_ok());
1112 }
1113
1114 #[tokio::test]
1115 async fn test_execute_validate_success() {
1116 let bot = dummy_bot();
1118 let res = execute_validate(&bot).await;
1119 assert!(res.is_ok() || res.is_err());
1121 }
1122
1123 #[tokio::test]
1124 async fn test_execute_completion_success() {
1125 let res =
1127 execute_completion(crate::completion::CompletionShell::Bash, None, false, false).await;
1128 assert!(res.is_ok());
1129 }
1130
1131 #[tokio::test]
1132 #[ignore]
1133 async fn test_execute_setup_mocked() {
1134 let res = execute_setup().await;
1137 assert!(res.is_ok() || res.is_err());
1139 }
1140
1141 #[tokio::test]
1143 async fn test_execute_config_show_missing_file() {
1144 let config_path = get_existing_config_path();
1146 let backup = if let Some(ref config_path) = config_path {
1147 let backup = config_path.with_extension("bak");
1148 fs::rename(config_path, &backup).ok();
1149 Some((config_path.clone(), backup))
1150 } else {
1151 None
1152 };
1153 let res = execute_config(true, false, false).await;
1155 assert!(res.is_ok());
1156 if let Some((config_path, backup)) = backup {
1158 fs::rename(&backup, &config_path).ok();
1159 }
1160 }
1161
1162 #[tokio::test]
1164 async fn test_execute_config_show_corrupted_file() {
1165 let dir = tempdir().unwrap();
1166 let path = dir.path().join("config.toml");
1167 fs::write(&path, "not a toml").unwrap();
1168 let res = toml::from_str::<Config>("not a toml");
1171 assert!(res.is_err());
1172 }
1173
1174 #[tokio::test]
1176 async fn test_execute_config_save_error() {
1177 let dir = tempdir().unwrap();
1178 let path = dir.path().join("readonly.toml");
1179 fs::write(&path, "").unwrap();
1180 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
1181 let config = Config::default();
1182 let res = config.save(Some(&path));
1183 assert!(res.is_err());
1184 }
1185
1186 #[tokio::test]
1188 async fn test_execute_completion_invalid_shell() {
1189 let res = execute_completion(
1192 crate::completion::CompletionShell::Bash,
1193 Some("/invalid/path/doesnotexist"),
1194 false,
1195 false,
1196 )
1197 .await;
1198 assert!(res.is_err());
1199 }
1200
1201 #[tokio::test]
1203 async fn test_execute_validate_missing_fields() {
1204 let config_path = get_existing_config_path();
1206 let backup = if let Some(ref config_path) = config_path {
1207 let backup = config_path.with_extension("bak");
1208 fs::rename(config_path, &backup).ok();
1209 Some((config_path.clone(), backup))
1210 } else {
1211 None
1212 };
1213 let bot = dummy_bot();
1214 let res = execute_validate(&bot).await;
1215 assert!(res.is_ok() || res.is_err());
1217 if let Some((config_path, backup)) = backup {
1219 fs::rename(&backup, &config_path).ok();
1220 }
1221 }
1222
1223 #[tokio::test]
1225 async fn test_execute_config_no_flags() {
1226 let res = execute_config(false, false, false).await;
1227 assert!(res.is_ok());
1228 }
1229
1230 #[tokio::test]
1240 async fn test_execute_config_toml_deserialize_error() {
1241 let result = toml::from_str::<Config>("invalid = toml");
1243 assert!(result.is_err());
1244 }
1245
1246 }