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 } => {
40 match handlers::handle_analyze(path, json, detailed, display, only, color_scheme) {
41 Ok(_output) => Ok(()), 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(|_| ()) }
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: _, ag_ui: _, ag_ui_port: _, } => {
176 use agent::ProviderType;
177 use cli::ChatProvider;
178 use config::load_agent_config;
179
180 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 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 match selector.load_conversation(&session_info) {
209 Ok(record) => {
210 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 }
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 let agent_config = load_agent_config();
246
247 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 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, };
264 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 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 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 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 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 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 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 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 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 unreachable!("Deploy commands should be handled in main.rs")
714 }
715 Commands::Env { .. } => {
716 unreachable!("Env commands should be handled in main.rs")
718 }
719 }
720}