Skip to main content

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