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