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 platform; // Platform session state for project/org context
12pub mod telemetry; // Add telemetry module
13pub mod wizard; // Interactive deployment wizard
14
15// Re-export commonly used types and functions
16pub use analyzer::{ProjectAnalysis, analyze_project};
17use cli::Commands;
18pub use error::{IaCGeneratorError, Result};
19pub use generator::{generate_compose, generate_dockerfile, generate_terraform};
20pub use handlers::*;
21pub use telemetry::{TelemetryClient, TelemetryConfig, UserId}; // Re-export telemetry types
22
23/// The current version of the CLI tool
24pub const VERSION: &str = env!("CARGO_PKG_VERSION");
25
26pub async fn run_command(command: Commands) -> Result<()> {
27    match command {
28        Commands::Analyze {
29            path,
30            json,
31            detailed,
32            display,
33            only,
34            color_scheme,
35        } => {
36            match handlers::handle_analyze(path, json, detailed, display, only, color_scheme) {
37                Ok(_output) => Ok(()), // The output was already printed by display_analysis_with_return
38                Err(e) => Err(e),
39            }
40        }
41        Commands::Generate {
42            path,
43            output,
44            dockerfile,
45            compose,
46            terraform,
47            all,
48            dry_run,
49            force,
50        } => handlers::handle_generate(
51            path, output, dockerfile, compose, terraform, all, dry_run, force,
52        ),
53        Commands::Validate { path, types, fix } => handlers::handle_validate(path, types, fix),
54        Commands::Support {
55            languages,
56            frameworks,
57            detailed,
58        } => handlers::handle_support(languages, frameworks, detailed),
59        Commands::Dependencies {
60            path,
61            licenses,
62            vulnerabilities,
63            prod_only,
64            dev_only,
65            format,
66        } => handlers::handle_dependencies(
67            path,
68            licenses,
69            vulnerabilities,
70            prod_only,
71            dev_only,
72            format,
73        )
74        .await
75        .map(|_| ()),
76        Commands::Vulnerabilities {
77            path,
78            severity,
79            format,
80            output,
81        } => handlers::handle_vulnerabilities(path, severity, format, output).await,
82        Commands::Security {
83            path,
84            mode,
85            include_low,
86            no_secrets,
87            no_code_patterns,
88            no_infrastructure,
89            no_compliance,
90            frameworks,
91            format,
92            output,
93            fail_on_findings,
94        } => {
95            handlers::handle_security(
96                path,
97                mode,
98                include_low,
99                no_secrets,
100                no_code_patterns,
101                no_infrastructure,
102                no_compliance,
103                frameworks,
104                format,
105                output,
106                fail_on_findings,
107            )
108            .map(|_| ()) // Map Result<String> to Result<()>
109        }
110        Commands::Tools { command } => handlers::handle_tools(command).await,
111        Commands::Optimize {
112            path,
113            cluster,
114            prometheus,
115            namespace,
116            period,
117            severity,
118            threshold,
119            safety_margin,
120            include_info,
121            include_system,
122            format,
123            output,
124            fix,
125            full,
126            apply,
127            dry_run,
128            backup_dir,
129            min_confidence,
130            cloud_provider,
131            region,
132        } => {
133            let format_str = match format {
134                cli::OutputFormat::Table => "table",
135                cli::OutputFormat::Json => "json",
136            };
137
138            let options = handlers::OptimizeOptions {
139                cluster,
140                prometheus,
141                namespace,
142                period,
143                severity,
144                threshold,
145                safety_margin,
146                include_info,
147                include_system,
148                format: format_str.to_string(),
149                output: output.map(|p| p.to_string_lossy().to_string()),
150                fix,
151                full,
152                apply,
153                dry_run,
154                backup_dir: backup_dir.map(|p| p.to_string_lossy().to_string()),
155                min_confidence,
156                cloud_provider,
157                region,
158            };
159
160            handlers::handle_optimize(&path, options).await
161        }
162        Commands::Chat {
163            path,
164            provider,
165            model,
166            query,
167            resume,
168            list_sessions: _, // Handled in main.rs
169        } => {
170            use agent::ProviderType;
171            use cli::ChatProvider;
172            use config::load_agent_config;
173
174            // Check if user is authenticated with Syncable
175            if !auth::credentials::is_authenticated() {
176                println!("\n\x1b[1;33m📢 Sign in to use Syncable Agent\x1b[0m");
177                println!("   It's free and costs you nothing!\n");
178                println!("   Run: \x1b[1;36msync-ctl auth login\x1b[0m\n");
179                return Err(error::IaCGeneratorError::Config(
180                    error::ConfigError::MissingConfig(
181                        "Syncable authentication required".to_string(),
182                    ),
183                ));
184            }
185
186            let project_path = path.canonicalize().unwrap_or(path);
187
188            // Handle --resume flag
189            if let Some(ref resume_arg) = resume {
190                use agent::persistence::{SessionSelector, format_relative_time};
191
192                let selector = SessionSelector::new(&project_path);
193                if let Some(session_info) = selector.resolve_session(resume_arg) {
194                    let time = format_relative_time(session_info.last_updated);
195                    println!(
196                        "\nResuming session: {} ({}, {} messages)",
197                        session_info.display_name, time, session_info.message_count
198                    );
199                    println!("Session ID: {}\n", session_info.id);
200
201                    // Load the session
202                    match selector.load_conversation(&session_info) {
203                        Ok(record) => {
204                            // Display previous messages as context
205                            println!("--- Previous conversation ---");
206                            for msg in record.messages.iter().take(5) {
207                                let role = match msg.role {
208                                    agent::persistence::MessageRole::User => "You",
209                                    agent::persistence::MessageRole::Assistant => "AI",
210                                    agent::persistence::MessageRole::System => "System",
211                                };
212                                let preview = if msg.content.len() > 100 {
213                                    format!("{}...", &msg.content[..100])
214                                } else {
215                                    msg.content.clone()
216                                };
217                                println!("  {}: {}", role, preview);
218                            }
219                            if record.messages.len() > 5 {
220                                println!("  ... and {} more messages", record.messages.len() - 5);
221                            }
222                            println!("--- End of history ---\n");
223                            // TODO: Load history into conversation context
224                        }
225                        Err(e) => {
226                            eprintln!("Warning: Failed to load session history: {}", e);
227                        }
228                    }
229                } else {
230                    eprintln!(
231                        "Session '{}' not found. Use --list-sessions to see available sessions.",
232                        resume_arg
233                    );
234                    return Ok(());
235                }
236            }
237
238            // Load saved config for Auto mode
239            let agent_config = load_agent_config();
240
241            // Determine provider - use saved default if Auto
242            let (provider_type, effective_model) = match provider {
243                ChatProvider::Openai => (ProviderType::OpenAI, model),
244                ChatProvider::Anthropic => (ProviderType::Anthropic, model),
245                ChatProvider::Bedrock => (ProviderType::Bedrock, model),
246                ChatProvider::Ollama => {
247                    eprintln!("Ollama support coming soon. Using OpenAI as fallback.");
248                    (ProviderType::OpenAI, model)
249                }
250                ChatProvider::Auto => {
251                    // Load from saved config
252                    let saved_provider = match agent_config.default_provider.as_str() {
253                        "openai" => ProviderType::OpenAI,
254                        "anthropic" => ProviderType::Anthropic,
255                        "bedrock" => ProviderType::Bedrock,
256                        _ => ProviderType::OpenAI, // Fallback
257                    };
258                    // Use saved model if no explicit model provided
259                    let saved_model = if model.is_some() {
260                        model
261                    } else {
262                        agent_config.default_model.clone()
263                    };
264                    (saved_provider, saved_model)
265                }
266            };
267
268            // Load API key/credentials from config to environment
269            // This is essential for Bedrock bearer token auth!
270            agent::session::ChatSession::load_api_key_to_env(provider_type);
271
272            if let Some(q) = query {
273                let response =
274                    agent::run_query(&project_path, &q, provider_type, effective_model).await?;
275                println!("{}", response);
276                Ok(())
277            } else {
278                agent::run_interactive(&project_path, provider_type, effective_model).await?;
279                Ok(())
280            }
281        }
282        Commands::Project { command } => {
283            use cli::{OutputFormat, ProjectCommand};
284            use platform::api::client::PlatformApiClient;
285            use platform::session::PlatformSession;
286
287            match command {
288                ProjectCommand::List { org_id, format } => {
289                    // Get org_id from argument or session
290                    let effective_org_id = match org_id {
291                        Some(id) => id,
292                        None => {
293                            let session = PlatformSession::load().unwrap_or_default();
294                            match session.org_id {
295                                Some(id) => id,
296                                None => {
297                                    eprintln!("No organization selected.");
298                                    eprintln!("Run: sync-ctl org list");
299                                    eprintln!("Then: sync-ctl org select <id>");
300                                    return Ok(());
301                                }
302                            }
303                        }
304                    };
305
306                    let client = PlatformApiClient::new().map_err(|e| {
307                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
308                            e.to_string(),
309                        ))
310                    })?;
311
312                    match client.list_projects(&effective_org_id).await {
313                        Ok(projects) => {
314                            if projects.is_empty() {
315                                println!("No projects found in this organization.");
316                                return Ok(());
317                            }
318
319                            match format {
320                                OutputFormat::Json => {
321                                    println!("{}", serde_json::to_string_pretty(&projects).unwrap_or_default());
322                                }
323                                OutputFormat::Table => {
324                                    println!("\n{:<40} {:<30} {}", "ID", "NAME", "DESCRIPTION");
325                                    println!("{}", "-".repeat(90));
326                                    for project in projects {
327                                        let desc = if project.description.is_empty() { "-" } else { &project.description };
328                                        let desc_truncated = if desc.len() > 30 {
329                                            format!("{}...", &desc[..27])
330                                        } else {
331                                            desc.to_string()
332                                        };
333                                        println!("{:<40} {:<30} {}", project.id, project.name, desc_truncated);
334                                    }
335                                    println!();
336                                }
337                            }
338                        }
339                        Err(platform::api::error::PlatformApiError::Unauthorized) => {
340                            eprintln!("Not authenticated. Run: sync-ctl auth login");
341                        }
342                        Err(e) => {
343                            eprintln!("Failed to list projects: {}", e);
344                        }
345                    }
346                    Ok(())
347                }
348                ProjectCommand::Select { id } => {
349                    let client = PlatformApiClient::new().map_err(|e| {
350                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
351                            e.to_string(),
352                        ))
353                    })?;
354
355                    match client.get_project(&id).await {
356                        Ok(project) => {
357                            // Get org info
358                            let org = client.get_organization(&project.organization_id).await.ok();
359                            let org_name = org.as_ref().map(|o| o.name.clone()).unwrap_or_else(|| "Unknown".to_string());
360
361                            let session = PlatformSession::with_project(
362                                project.id.clone(),
363                                project.name.clone(),
364                                project.organization_id.clone(),
365                                org_name.clone(),
366                            );
367
368                            if let Err(e) = session.save() {
369                                eprintln!("Warning: Failed to save session: {}", e);
370                            }
371
372                            println!("✓ Selected project: {} ({})", project.name, project.id);
373                            println!("  Organization: {} ({})", org_name, project.organization_id);
374                        }
375                        Err(platform::api::error::PlatformApiError::Unauthorized) => {
376                            eprintln!("Not authenticated. Run: sync-ctl auth login");
377                        }
378                        Err(platform::api::error::PlatformApiError::NotFound(_)) => {
379                            eprintln!("Project not found: {}", id);
380                            eprintln!("Run: sync-ctl project list");
381                        }
382                        Err(e) => {
383                            eprintln!("Failed to select project: {}", e);
384                        }
385                    }
386                    Ok(())
387                }
388                ProjectCommand::Current => {
389                    let session = PlatformSession::load().unwrap_or_default();
390
391                    if !session.is_project_selected() {
392                        println!("No project selected.");
393                        println!("\nTo select a project:");
394                        println!("  1. sync-ctl org list");
395                        println!("  2. sync-ctl org select <org-id>");
396                        println!("  3. sync-ctl project list");
397                        println!("  4. sync-ctl project select <project-id>");
398                        return Ok(());
399                    }
400
401                    println!("\nCurrent context: {}", session.display_context());
402                    if let (Some(org_name), Some(org_id)) = (&session.org_name, &session.org_id) {
403                        println!("  Organization: {} ({})", org_name, org_id);
404                    }
405                    if let (Some(project_name), Some(project_id)) = (&session.project_name, &session.project_id) {
406                        println!("  Project:      {} ({})", project_name, project_id);
407                    }
408                    if let (Some(env_name), Some(env_id)) = (&session.environment_name, &session.environment_id) {
409                        println!("  Environment:  {} ({})", env_name, env_id);
410                    } else {
411                        println!("  Environment:  (none selected)");
412                        println!("\n  To select an environment:");
413                        println!("    sync-ctl env list");
414                        println!("    sync-ctl env select <env-id>");
415                    }
416                    if let Some(updated) = session.last_updated {
417                        println!("  Last updated: {}", updated.format("%Y-%m-%d %H:%M:%S UTC"));
418                    }
419                    println!();
420                    Ok(())
421                }
422                ProjectCommand::Info { id } => {
423                    // Get project id from arg or session
424                    let project_id = match id {
425                        Some(id) => id,
426                        None => {
427                            let session = PlatformSession::load().unwrap_or_default();
428                            match session.project_id {
429                                Some(id) => id,
430                                None => {
431                                    eprintln!("No project specified or selected.");
432                                    eprintln!("Run: sync-ctl project select <id>");
433                                    return Ok(());
434                                }
435                            }
436                        }
437                    };
438
439                    let client = PlatformApiClient::new().map_err(|e| {
440                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
441                            e.to_string(),
442                        ))
443                    })?;
444
445                    match client.get_project(&project_id).await {
446                        Ok(project) => {
447                            // Get org info
448                            let org = client.get_organization(&project.organization_id).await.ok();
449                            let org_name = org.as_ref().map(|o| o.name.clone()).unwrap_or_else(|| "Unknown".to_string());
450
451                            println!("\nProject Details:");
452                            println!("  ID:           {}", project.id);
453                            println!("  Name:         {}", project.name);
454                            let desc = if project.description.is_empty() { "-" } else { &project.description };
455                            println!("  Description:  {}", desc);
456                            println!("  Organization: {} ({})", org_name, project.organization_id);
457                            println!("  Created:      {}", project.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
458                            println!();
459                        }
460                        Err(platform::api::error::PlatformApiError::Unauthorized) => {
461                            eprintln!("Not authenticated. Run: sync-ctl auth login");
462                        }
463                        Err(platform::api::error::PlatformApiError::NotFound(_)) => {
464                            eprintln!("Project not found: {}", project_id);
465                        }
466                        Err(e) => {
467                            eprintln!("Failed to get project info: {}", e);
468                        }
469                    }
470                    Ok(())
471                }
472            }
473        }
474        Commands::Org { command } => {
475            use cli::{OutputFormat, OrgCommand};
476            use platform::api::client::PlatformApiClient;
477            use platform::session::PlatformSession;
478
479            match command {
480                OrgCommand::List { format } => {
481                    let client = PlatformApiClient::new().map_err(|e| {
482                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
483                            e.to_string(),
484                        ))
485                    })?;
486
487                    match client.list_organizations().await {
488                        Ok(orgs) => {
489                            if orgs.is_empty() {
490                                println!("No organizations found.");
491                                return Ok(());
492                            }
493
494                            match format {
495                                OutputFormat::Json => {
496                                    println!("{}", serde_json::to_string_pretty(&orgs).unwrap_or_default());
497                                }
498                                OutputFormat::Table => {
499                                    println!("\n{:<40} {:<30} {}", "ID", "NAME", "SLUG");
500                                    println!("{}", "-".repeat(90));
501                                    for org in orgs {
502                                        let slug = if org.slug.is_empty() { "-" } else { &org.slug };
503                                        println!("{:<40} {:<30} {}", org.id, org.name, slug);
504                                    }
505                                    println!();
506                                }
507                            }
508                        }
509                        Err(platform::api::error::PlatformApiError::Unauthorized) => {
510                            eprintln!("Not authenticated. Run: sync-ctl auth login");
511                        }
512                        Err(e) => {
513                            eprintln!("Failed to list organizations: {}", e);
514                        }
515                    }
516                    Ok(())
517                }
518                OrgCommand::Select { id } => {
519                    let client = PlatformApiClient::new().map_err(|e| {
520                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
521                            e.to_string(),
522                        ))
523                    })?;
524
525                    match client.get_organization(&id).await {
526                        Ok(org) => {
527                            // Create session with org only (clear any project/env selection)
528                            let session = PlatformSession {
529                                project_id: None,
530                                project_name: None,
531                                org_id: Some(org.id.clone()),
532                                org_name: Some(org.name.clone()),
533                                environment_id: None,
534                                environment_name: None,
535                                last_updated: Some(chrono::Utc::now()),
536                            };
537
538                            if let Err(e) = session.save() {
539                                eprintln!("Warning: Failed to save session: {}", e);
540                            }
541
542                            println!("✓ Selected organization: {} ({})", org.name, org.id);
543                            println!("\nNext: Run 'sync-ctl project list' to see projects");
544                        }
545                        Err(platform::api::error::PlatformApiError::Unauthorized) => {
546                            eprintln!("Not authenticated. Run: sync-ctl auth login");
547                        }
548                        Err(platform::api::error::PlatformApiError::NotFound(_)) => {
549                            eprintln!("Organization not found: {}", id);
550                            eprintln!("Run: sync-ctl org list");
551                        }
552                        Err(e) => {
553                            eprintln!("Failed to select organization: {}", e);
554                        }
555                    }
556                    Ok(())
557                }
558            }
559        }
560        Commands::Auth { command } => {
561            use auth::credentials;
562            use auth::device_flow;
563            use cli::AuthCommand;
564
565            match command {
566                AuthCommand::Login { no_browser } => {
567                    device_flow::login(no_browser).await.map_err(|e| {
568                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
569                            e.to_string(),
570                        ))
571                    })
572                }
573                AuthCommand::Logout => {
574                    credentials::clear_credentials().map_err(|e| {
575                        error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(
576                            e.to_string(),
577                        ))
578                    })?;
579                    println!("✅ Logged out successfully. Credentials cleared.");
580                    Ok(())
581                }
582                AuthCommand::Status => {
583                    match credentials::get_auth_status() {
584                        credentials::AuthStatus::NotAuthenticated => {
585                            println!("❌ Not logged in.");
586                            println!("   Run: sync-ctl auth login");
587                        }
588                        credentials::AuthStatus::Expired => {
589                            println!("⚠️  Session expired.");
590                            println!("   Run: sync-ctl auth login");
591                        }
592                        credentials::AuthStatus::Authenticated { email, expires_at } => {
593                            println!("✅ Logged in");
594                            if let Some(e) = email {
595                                println!("   Email: {}", e);
596                            }
597                            if let Some(exp) = expires_at {
598                                let now = std::time::SystemTime::now()
599                                    .duration_since(std::time::UNIX_EPOCH)
600                                    .map(|d| d.as_secs())
601                                    .unwrap_or(0);
602                                if exp > now {
603                                    let remaining = exp - now;
604                                    let days = remaining / 86400;
605                                    let hours = (remaining % 86400) / 3600;
606                                    println!("   Expires in: {}d {}h", days, hours);
607                                }
608                            }
609                        }
610                    }
611                    Ok(())
612                }
613                AuthCommand::Token { raw } => match credentials::get_access_token() {
614                    Some(token) => {
615                        if raw {
616                            print!("{}", token);
617                        } else {
618                            println!("Access Token: {}", token);
619                        }
620                        Ok(())
621                    }
622                    None => {
623                        eprintln!("Not authenticated. Run: sync-ctl auth login");
624                        std::process::exit(1);
625                    }
626                },
627            }
628        }
629        Commands::Deploy { .. } => {
630            // Deploy commands are handled in main.rs directly
631            unreachable!("Deploy commands should be handled in main.rs")
632        }
633        Commands::Env { .. } => {
634            // Env commands are handled in main.rs directly
635            unreachable!("Env commands should be handled in main.rs")
636        }
637    }
638}