1pub mod agent;
2pub mod analyzer;
3pub mod auth; pub mod bedrock; pub mod cli;
6pub mod common;
7pub mod config;
8pub mod error;
9pub mod generator;
10pub mod handlers;
11pub mod platform; pub mod server; pub mod telemetry; pub mod wizard; pub 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}; pub 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(()), 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(|_| ()) }
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: _, ag_ui: _, ag_ui_port: _, } => {
191 use agent::ProviderType;
192 use cli::ChatProvider;
193 use config::load_agent_config;
194
195 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 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 match selector.load_conversation(&session_info) {
224 Ok(record) => {
225 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 }
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 let agent_config = load_agent_config();
261
262 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 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, };
279 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 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 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 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 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 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 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 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 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 unreachable!("Retrieve commands should be handled in main.rs")
729 }
730 Commands::Deploy { .. } => {
731 unreachable!("Deploy commands should be handled in main.rs")
733 }
734 Commands::Env { .. } => {
735 unreachable!("Env commands should be handled in main.rs")
737 }
738 }
739}