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::Optimize {
110            path,
111            cluster,
112            prometheus,
113            namespace,
114            period,
115            severity,
116            threshold,
117            safety_margin,
118            include_info,
119            include_system,
120            format,
121            output,
122            fix,
123            full,
124            apply,
125            dry_run,
126            backup_dir,
127            min_confidence,
128            cloud_provider,
129            region,
130        } => {
131            let format_str = match format {
132                cli::OutputFormat::Table => "table",
133                cli::OutputFormat::Json => "json",
134            };
135
136            let options = handlers::OptimizeOptions {
137                cluster,
138                prometheus,
139                namespace,
140                period,
141                severity,
142                threshold,
143                safety_margin,
144                include_info,
145                include_system,
146                format: format_str.to_string(),
147                output: output.map(|p| p.to_string_lossy().to_string()),
148                fix,
149                full,
150                apply,
151                dry_run,
152                backup_dir: backup_dir.map(|p| p.to_string_lossy().to_string()),
153                min_confidence,
154                cloud_provider,
155                region,
156            };
157
158            handlers::handle_optimize(&path, options).await
159        }
160        Commands::Chat {
161            path,
162            provider,
163            model,
164            query,
165            resume,
166            list_sessions: _, // Handled in main.rs
167        } => {
168            use agent::ProviderType;
169            use cli::ChatProvider;
170            use config::load_agent_config;
171
172            // Check if user is authenticated with Syncable
173            if !auth::credentials::is_authenticated() {
174                println!("\n\x1b[1;33m📢 Sign in to use Syncable Agent\x1b[0m");
175                println!("   It's free and costs you nothing!\n");
176                println!("   Run: \x1b[1;36msync-ctl auth login\x1b[0m\n");
177                return Err(error::IaCGeneratorError::Config(
178                    error::ConfigError::MissingConfig(
179                        "Syncable authentication required".to_string(),
180                    ),
181                ));
182            }
183
184            let project_path = path.canonicalize().unwrap_or(path);
185
186            // Handle --resume flag
187            if let Some(ref resume_arg) = resume {
188                use agent::persistence::{SessionSelector, format_relative_time};
189
190                let selector = SessionSelector::new(&project_path);
191                if let Some(session_info) = selector.resolve_session(resume_arg) {
192                    let time = format_relative_time(session_info.last_updated);
193                    println!(
194                        "\nResuming session: {} ({}, {} messages)",
195                        session_info.display_name, time, session_info.message_count
196                    );
197                    println!("Session ID: {}\n", session_info.id);
198
199                    // Load the session
200                    match selector.load_conversation(&session_info) {
201                        Ok(record) => {
202                            // Display previous messages as context
203                            println!("--- Previous conversation ---");
204                            for msg in record.messages.iter().take(5) {
205                                let role = match msg.role {
206                                    agent::persistence::MessageRole::User => "You",
207                                    agent::persistence::MessageRole::Assistant => "AI",
208                                    agent::persistence::MessageRole::System => "System",
209                                };
210                                let preview = if msg.content.len() > 100 {
211                                    format!("{}...", &msg.content[..100])
212                                } else {
213                                    msg.content.clone()
214                                };
215                                println!("  {}: {}", role, preview);
216                            }
217                            if record.messages.len() > 5 {
218                                println!("  ... and {} more messages", record.messages.len() - 5);
219                            }
220                            println!("--- End of history ---\n");
221                            // TODO: Load history into conversation context
222                        }
223                        Err(e) => {
224                            eprintln!("Warning: Failed to load session history: {}", e);
225                        }
226                    }
227                } else {
228                    eprintln!(
229                        "Session '{}' not found. Use --list-sessions to see available sessions.",
230                        resume_arg
231                    );
232                    return Ok(());
233                }
234            }
235
236            // Load saved config for Auto mode
237            let agent_config = load_agent_config();
238
239            // Determine provider - use saved default if Auto
240            let (provider_type, effective_model) = match provider {
241                ChatProvider::Openai => (ProviderType::OpenAI, model),
242                ChatProvider::Anthropic => (ProviderType::Anthropic, model),
243                ChatProvider::Bedrock => (ProviderType::Bedrock, model),
244                ChatProvider::Ollama => {
245                    eprintln!("Ollama support coming soon. Using OpenAI as fallback.");
246                    (ProviderType::OpenAI, model)
247                }
248                ChatProvider::Auto => {
249                    // Load from saved config
250                    let saved_provider = match agent_config.default_provider.as_str() {
251                        "openai" => ProviderType::OpenAI,
252                        "anthropic" => ProviderType::Anthropic,
253                        "bedrock" => ProviderType::Bedrock,
254                        _ => ProviderType::OpenAI, // Fallback
255                    };
256                    // Use saved model if no explicit model provided
257                    let saved_model = if model.is_some() {
258                        model
259                    } else {
260                        agent_config.default_model.clone()
261                    };
262                    (saved_provider, saved_model)
263                }
264            };
265
266            // Load API key/credentials from config to environment
267            // This is essential for Bedrock bearer token auth!
268            agent::session::ChatSession::load_api_key_to_env(provider_type);
269
270            if let Some(q) = query {
271                let response =
272                    agent::run_query(&project_path, &q, provider_type, effective_model).await?;
273                println!("{}", response);
274                Ok(())
275            } else {
276                agent::run_interactive(&project_path, provider_type, effective_model).await?;
277                Ok(())
278            }
279        }
280        Commands::Auth { command } => {
281            use auth::credentials;
282            use auth::device_flow;
283            use cli::AuthCommand;
284
285            match command {
286                AuthCommand::Login { no_browser } => {
287                    device_flow::login(no_browser).await.map_err(|e| {
288                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
289                            e.to_string(),
290                        ))
291                    })
292                }
293                AuthCommand::Logout => {
294                    credentials::clear_credentials().map_err(|e| {
295                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
296                            e.to_string(),
297                        ))
298                    })?;
299                    println!("✅ Logged out successfully. Credentials cleared.");
300                    Ok(())
301                }
302                AuthCommand::Status => {
303                    match credentials::get_auth_status() {
304                        credentials::AuthStatus::NotAuthenticated => {
305                            println!("❌ Not logged in.");
306                            println!("   Run: sync-ctl auth login");
307                        }
308                        credentials::AuthStatus::Expired => {
309                            println!("⚠️  Session expired.");
310                            println!("   Run: sync-ctl auth login");
311                        }
312                        credentials::AuthStatus::Authenticated { email, expires_at } => {
313                            println!("✅ Logged in");
314                            if let Some(e) = email {
315                                println!("   Email: {}", e);
316                            }
317                            if let Some(exp) = expires_at {
318                                let now = std::time::SystemTime::now()
319                                    .duration_since(std::time::UNIX_EPOCH)
320                                    .map(|d| d.as_secs())
321                                    .unwrap_or(0);
322                                if exp > now {
323                                    let remaining = exp - now;
324                                    let days = remaining / 86400;
325                                    let hours = (remaining % 86400) / 3600;
326                                    println!("   Expires in: {}d {}h", days, hours);
327                                }
328                            }
329                        }
330                    }
331                    Ok(())
332                }
333                AuthCommand::Token { raw } => match credentials::get_access_token() {
334                    Some(token) => {
335                        if raw {
336                            print!("{}", token);
337                        } else {
338                            println!("Access Token: {}", token);
339                        }
340                        Ok(())
341                    }
342                    None => {
343                        eprintln!("Not authenticated. Run: sync-ctl auth login");
344                        std::process::exit(1);
345                    }
346                },
347            }
348        }
349    }
350}