syncable_cli/
lib.rs

1pub mod agent;
2pub mod analyzer;
3pub mod auth; // Authentication module for Syncable platform
4pub mod bedrock; // Inlined rig-bedrock with extended thinking fixes
5pub mod cli;
6pub mod common;
7pub mod config;
8pub mod error;
9pub mod generator;
10pub mod handlers;
11pub mod telemetry; // Add telemetry module
12
13// Re-export commonly used types and functions
14pub use analyzer::{ProjectAnalysis, analyze_project};
15use cli::Commands;
16pub use error::{IaCGeneratorError, Result};
17pub use generator::{generate_compose, generate_dockerfile, generate_terraform};
18pub use handlers::*;
19pub use telemetry::{TelemetryClient, TelemetryConfig, UserId}; // Re-export telemetry types
20
21/// The current version of the CLI tool
22pub const VERSION: &str = env!("CARGO_PKG_VERSION");
23
24pub async fn run_command(command: Commands) -> Result<()> {
25    match command {
26        Commands::Analyze {
27            path,
28            json,
29            detailed,
30            display,
31            only,
32            color_scheme,
33        } => {
34            match handlers::handle_analyze(path, json, detailed, display, only, color_scheme) {
35                Ok(_output) => Ok(()), // The output was already printed by display_analysis_with_return
36                Err(e) => Err(e),
37            }
38        }
39        Commands::Generate {
40            path,
41            output,
42            dockerfile,
43            compose,
44            terraform,
45            all,
46            dry_run,
47            force,
48        } => handlers::handle_generate(
49            path, output, dockerfile, compose, terraform, all, dry_run, force,
50        ),
51        Commands::Validate { path, types, fix } => handlers::handle_validate(path, types, fix),
52        Commands::Support {
53            languages,
54            frameworks,
55            detailed,
56        } => handlers::handle_support(languages, frameworks, detailed),
57        Commands::Dependencies {
58            path,
59            licenses,
60            vulnerabilities,
61            prod_only,
62            dev_only,
63            format,
64        } => handlers::handle_dependencies(
65            path,
66            licenses,
67            vulnerabilities,
68            prod_only,
69            dev_only,
70            format,
71        )
72        .await
73        .map(|_| ()),
74        Commands::Vulnerabilities {
75            path,
76            severity,
77            format,
78            output,
79        } => handlers::handle_vulnerabilities(path, severity, format, output).await,
80        Commands::Security {
81            path,
82            mode,
83            include_low,
84            no_secrets,
85            no_code_patterns,
86            no_infrastructure,
87            no_compliance,
88            frameworks,
89            format,
90            output,
91            fail_on_findings,
92        } => {
93            handlers::handle_security(
94                path,
95                mode,
96                include_low,
97                no_secrets,
98                no_code_patterns,
99                no_infrastructure,
100                no_compliance,
101                frameworks,
102                format,
103                output,
104                fail_on_findings,
105            )
106            .map(|_| ()) // Map Result<String> to Result<()>
107        }
108        Commands::Tools { command } => handlers::handle_tools(command).await,
109        Commands::Chat {
110            path,
111            provider,
112            model,
113            query,
114            resume,
115            list_sessions: _, // Handled in main.rs
116        } => {
117            use agent::ProviderType;
118            use cli::ChatProvider;
119            use config::load_agent_config;
120
121            let project_path = path.canonicalize().unwrap_or(path);
122
123            // Handle --resume flag
124            if let Some(ref resume_arg) = resume {
125                use agent::persistence::{SessionSelector, format_relative_time};
126
127                let selector = SessionSelector::new(&project_path);
128                if let Some(session_info) = selector.resolve_session(resume_arg) {
129                    let time = format_relative_time(session_info.last_updated);
130                    println!(
131                        "\nResuming session: {} ({}, {} messages)",
132                        session_info.display_name, time, session_info.message_count
133                    );
134                    println!("Session ID: {}\n", session_info.id);
135
136                    // Load the session
137                    match selector.load_conversation(&session_info) {
138                        Ok(record) => {
139                            // Display previous messages as context
140                            println!("--- Previous conversation ---");
141                            for msg in record.messages.iter().take(5) {
142                                let role = match msg.role {
143                                    agent::persistence::MessageRole::User => "You",
144                                    agent::persistence::MessageRole::Assistant => "AI",
145                                    agent::persistence::MessageRole::System => "System",
146                                };
147                                let preview = if msg.content.len() > 100 {
148                                    format!("{}...", &msg.content[..100])
149                                } else {
150                                    msg.content.clone()
151                                };
152                                println!("  {}: {}", role, preview);
153                            }
154                            if record.messages.len() > 5 {
155                                println!("  ... and {} more messages", record.messages.len() - 5);
156                            }
157                            println!("--- End of history ---\n");
158                            // TODO: Load history into conversation context
159                        }
160                        Err(e) => {
161                            eprintln!("Warning: Failed to load session history: {}", e);
162                        }
163                    }
164                } else {
165                    eprintln!(
166                        "Session '{}' not found. Use --list-sessions to see available sessions.",
167                        resume_arg
168                    );
169                    return Ok(());
170                }
171            }
172
173            // Load saved config for Auto mode
174            let agent_config = load_agent_config();
175
176            // Determine provider - use saved default if Auto
177            let (provider_type, effective_model) = match provider {
178                ChatProvider::Openai => (ProviderType::OpenAI, model),
179                ChatProvider::Anthropic => (ProviderType::Anthropic, model),
180                ChatProvider::Bedrock => (ProviderType::Bedrock, model),
181                ChatProvider::Ollama => {
182                    eprintln!("Ollama support coming soon. Using OpenAI as fallback.");
183                    (ProviderType::OpenAI, model)
184                }
185                ChatProvider::Auto => {
186                    // Load from saved config
187                    let saved_provider = match agent_config.default_provider.as_str() {
188                        "openai" => ProviderType::OpenAI,
189                        "anthropic" => ProviderType::Anthropic,
190                        "bedrock" => ProviderType::Bedrock,
191                        _ => ProviderType::OpenAI, // Fallback
192                    };
193                    // Use saved model if no explicit model provided
194                    let saved_model = if model.is_some() {
195                        model
196                    } else {
197                        agent_config.default_model.clone()
198                    };
199                    (saved_provider, saved_model)
200                }
201            };
202
203            // Load API key/credentials from config to environment
204            // This is essential for Bedrock bearer token auth!
205            agent::session::ChatSession::load_api_key_to_env(provider_type);
206
207            if let Some(q) = query {
208                let response =
209                    agent::run_query(&project_path, &q, provider_type, effective_model).await?;
210                println!("{}", response);
211                Ok(())
212            } else {
213                agent::run_interactive(&project_path, provider_type, effective_model).await?;
214                Ok(())
215            }
216        }
217        Commands::Auth { command } => {
218            use cli::AuthCommand;
219            use auth::credentials;
220            use auth::device_flow;
221            
222            match command {
223                AuthCommand::Login { no_browser } => {
224                    device_flow::login(no_browser).await.map_err(|e| {
225                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(e.to_string()))
226                    })
227                }
228                AuthCommand::Logout => {
229                    credentials::clear_credentials().map_err(|e| {
230                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(e.to_string()))
231                    })?;
232                    println!("✅ Logged out successfully. Credentials cleared.");
233                    Ok(())
234                }
235                AuthCommand::Status => {
236                    match credentials::get_auth_status() {
237                        credentials::AuthStatus::NotAuthenticated => {
238                            println!("❌ Not logged in.");
239                            println!("   Run: sync-ctl auth login");
240                        }
241                        credentials::AuthStatus::Expired => {
242                            println!("⚠️  Session expired.");
243                            println!("   Run: sync-ctl auth login");
244                        }
245                        credentials::AuthStatus::Authenticated { email, expires_at } => {
246                            println!("✅ Logged in");
247                            if let Some(e) = email {
248                                println!("   Email: {}", e);
249                            }
250                            if let Some(exp) = expires_at {
251                                let now = std::time::SystemTime::now()
252                                    .duration_since(std::time::UNIX_EPOCH)
253                                    .map(|d| d.as_secs())
254                                    .unwrap_or(0);
255                                if exp > now {
256                                    let remaining = exp - now;
257                                    let days = remaining / 86400;
258                                    let hours = (remaining % 86400) / 3600;
259                                    println!("   Expires in: {}d {}h", days, hours);
260                                }
261                            }
262                        }
263                    }
264                    Ok(())
265                }
266                AuthCommand::Token { raw } => {
267                    match credentials::get_access_token() {
268                        Some(token) => {
269                            if raw {
270                                print!("{}", token);
271                            } else {
272                                println!("Access Token: {}", token);
273                            }
274                            Ok(())
275                        }
276                        None => {
277                            eprintln!("Not authenticated. Run: sync-ctl auth login");
278                            std::process::exit(1);
279                        }
280                    }
281                }
282            }
283        }
284    }
285}