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