1use std::{env, process, str::FromStr};
4
5use clap::{CommandFactory, Parser};
6use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
7
8use crate::cli::{
9 Cli, Commands, FederationCommands, GenerateClientCommands, IntrospectCommands, MigrateCommands,
10 PerfCommands, PerfExploreCommands, SchemaCommands, ValidateCommands,
11};
12
13#[allow(clippy::cognitive_complexity)] pub async fn run() {
16 use crate::{commands, output};
17
18 if let Some(code) = handle_introspection_flags() {
19 process::exit(code);
20 }
21
22 let cli = Cli::parse();
23
24 init_logging(cli.verbose, cli.debug);
25
26 let json_output = cli.json;
27 let debug_output = cli.debug;
28
29 let result = match cli.command {
30 Commands::Compile {
31 input,
32 types,
33 schema_dir,
34 type_files,
35 query_files,
36 mutation_files,
37 output,
38 check,
39 skip_hash,
40 database,
41 emit_ddl,
42 check_migrations,
43 } => {
44 commands::compile::run(
45 &input,
46 types.as_deref(),
47 schema_dir.as_deref(),
48 type_files,
49 query_files,
50 mutation_files,
51 &output,
52 check,
53 database.as_deref(),
54 emit_ddl.as_deref(),
55 check_migrations,
56 skip_hash,
57 )
58 .await
59 },
60
61 Commands::Extract {
62 input,
63 language,
64 recursive,
65 output,
66 } => commands::extract::run(&input, language.as_deref(), recursive, &output),
67
68 Commands::Explain { query } => match commands::explain::run(&query) {
69 Ok(result) => {
70 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
71 Ok(())
72 },
73 Err(e) => Err(e),
74 },
75
76 Commands::Cost { query } => match commands::cost::run(&query) {
77 Ok(result) => {
78 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
79 Ok(())
80 },
81 Err(e) => Err(e),
82 },
83
84 Commands::Analyze { schema } => match commands::analyze::run(&schema) {
85 Ok(result) => {
86 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
87 Ok(())
88 },
89 Err(e) => Err(e),
90 },
91
92 Commands::DependencyGraph { schema, format } => {
93 match commands::dependency_graph::GraphFormat::from_str(&format) {
94 Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
95 Ok(result) => {
96 println!(
97 "{}",
98 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
99 );
100 Ok(())
101 },
102 Err(e) => Err(e),
103 },
104 Err(e) => Err(anyhow::anyhow!(e)),
105 }
106 },
107
108 Commands::Lint {
109 schema,
110 federation,
111 cost,
112 cache,
113 auth,
114 compilation,
115 fail_on_critical,
116 fail_on_warning,
117 } => {
118 let opts = commands::lint::LintOptions {
119 fail_on_critical,
120 fail_on_warning,
121 filter: commands::lint::LintCategoryFilter {
122 federation,
123 cost,
124 cache,
125 auth,
126 compilation,
127 },
128 };
129 match commands::lint::run(&schema, opts) {
130 Ok(result) => {
131 println!(
132 "{}",
133 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
134 );
135 enforce_exit_code(&result);
138 Ok(())
139 },
140 Err(e) => Err(e),
141 }
142 },
143
144 Commands::Federation { command } => match command {
145 FederationCommands::Graph { schema, format } => {
146 match commands::federation::graph::GraphFormat::from_str(&format) {
147 Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
148 Ok(result) => {
149 println!(
150 "{}",
151 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
152 );
153 Ok(())
154 },
155 Err(e) => Err(e),
156 },
157 Err(e) => Err(anyhow::anyhow!(e)),
158 }
159 },
160 FederationCommands::Check {
161 schema,
162 against,
163 json,
164 } => {
165 match commands::federation::check::run(
166 &schema,
167 against.as_deref(),
168 json || cli.json,
169 ) {
170 Ok(result) => {
171 if !(json || cli.json) {
176 println!(
177 "{}",
178 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
179 );
180 }
181 enforce_exit_code(&result);
184 Ok(())
185 },
186 Err(e) => Err(e),
187 }
188 },
189 },
190
191 Commands::GenerateViews {
192 schema,
193 entity,
194 view,
195 refresh_strategy,
196 output,
197 include_composition_views,
198 include_monitoring,
199 validate,
200 gen_verbose,
201 } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
202 Ok(refresh_strat) => {
203 let config = commands::generate_views::GenerateViewsConfig {
204 schema_path: schema,
205 entity,
206 view,
207 refresh_strategy: refresh_strat,
208 output,
209 include_composition_views,
210 include_monitoring,
211 validate_only: validate,
212 verbose: cli.verbose || gen_verbose,
213 };
214
215 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
216 commands::generate_views::run(config, &formatter)
217 },
218 Err(e) => Err(anyhow::anyhow!(e)),
219 },
220
221 Commands::GenerateCaptureTriggers {
222 schema,
223 output,
224 no_function,
225 } => {
226 let config = commands::generate_capture_triggers::GenerateCaptureTriggersConfig {
227 schema_path: schema,
228 output,
229 include_function: !no_function,
230 };
231 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
232 commands::generate_capture_triggers::run(config, &formatter)
233 },
234
235 Commands::Validate {
236 command,
237 input,
238 check_cycles,
239 check_unused,
240 strict,
241 types,
242 against_db,
243 } => match command {
244 Some(ValidateCommands::Facts { schema, database }) => {
245 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
246 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
247 .await
248 },
249 None => match (against_db, input) {
250 (Some(db_url), Some(input)) => {
251 commands::validate::run_against_db(&input, &db_url, cli.json).await
252 },
253 (Some(_), None) => Err(anyhow::anyhow!(
254 "INPUT (schema.compiled.json) is required with --against-db"
255 )),
256 (None, Some(input)) => {
257 let opts = commands::validate::ValidateOptions {
258 check_cycles,
259 check_unused,
260 strict,
261 filter_types: types,
262 };
263 match commands::validate::run_with_options(&input, opts) {
264 Ok(result) => {
265 println!(
266 "{}",
267 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
268 );
269 if result.status == "validation-failed" {
270 Err(anyhow::anyhow!("Validation failed"))
271 } else {
272 Ok(())
273 }
274 },
275 Err(e) => Err(e),
276 }
277 },
278 (None, None) => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
279 },
280 },
281
282 Commands::Introspect { command } => match command {
283 IntrospectCommands::Facts { database, format } => {
284 match commands::introspect_facts::OutputFormat::parse(&format) {
285 Ok(fmt) => {
286 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
287 commands::introspect_facts::run(&database, fmt, &formatter).await
288 },
289 Err(e) => Err(anyhow::anyhow!(e)),
290 }
291 },
292 },
293
294 Commands::Generate {
295 input,
296 language,
297 output,
298 } => match commands::init::Language::from_str(&language) {
299 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
300 Err(e) => Err(anyhow::anyhow!(e)),
301 },
302
303 Commands::GenerateClient { language } => match language {
304 GenerateClientCommands::Typescript { schema, out, force } => {
305 commands::generate_client::run(
306 commands::generate_client::ClientLanguage::TypeScript,
307 schema.as_deref(),
308 &out,
309 force,
310 )
311 },
312 },
313
314 Commands::Init {
315 project_name,
316 language,
317 database,
318 size,
319 no_git,
320 } => {
321 match (
322 commands::init::Language::from_str(&language),
323 commands::init::Database::from_str(&database),
324 commands::init::ProjectSize::from_str(&size),
325 ) {
326 (Ok(lang), Ok(db), Ok(sz)) => {
327 let config = commands::init::InitConfig {
328 project_name,
329 language: lang,
330 database: db,
331 size: sz,
332 no_git,
333 };
334 commands::init::run(&config)
335 },
336 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
337 }
338 },
339
340 Commands::Migrate { command } => {
341 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
342 match command {
343 MigrateCommands::Up { database, dir } => {
344 let db_url = commands::migrate::resolve_database_url(database.as_deref());
345 match db_url {
346 Ok(url) => {
347 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
348 let action = commands::migrate::MigrateAction::Up {
349 database_url: url,
350 dir: mig_dir,
351 };
352 commands::migrate::run(&action, &formatter)
353 },
354 Err(e) => Err(e),
355 }
356 },
357 MigrateCommands::Down {
358 database,
359 dir,
360 steps,
361 } => {
362 let db_url = commands::migrate::resolve_database_url(database.as_deref());
363 match db_url {
364 Ok(url) => {
365 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
366 let action = commands::migrate::MigrateAction::Down {
367 database_url: url,
368 dir: mig_dir,
369 steps,
370 };
371 commands::migrate::run(&action, &formatter)
372 },
373 Err(e) => Err(e),
374 }
375 },
376 MigrateCommands::Status { database, dir } => {
377 let db_url = commands::migrate::resolve_database_url(database.as_deref());
378 match db_url {
379 Ok(url) => {
380 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
381 let action = commands::migrate::MigrateAction::Status {
382 database_url: url,
383 dir: mig_dir,
384 };
385 commands::migrate::run(&action, &formatter)
386 },
387 Err(e) => Err(e),
388 }
389 },
390 MigrateCommands::Create { name, dir } => {
391 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
392 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
393 commands::migrate::run(&action, &formatter)
394 },
395 MigrateCommands::Generate { name, dir } => {
396 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
397 let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
398 commands::migrate::run(&action, &formatter)
399 },
400 MigrateCommands::Validate { dir } => {
401 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
402 let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
403 commands::migrate::run(&action, &formatter)
404 },
405 MigrateCommands::Preflight { dir } => {
406 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
407 let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
408 commands::migrate::run(&action, &formatter)
409 },
410 }
411 },
412
413 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
414 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
415 Err(e) => Err(anyhow::anyhow!(e)),
416 },
417
418 #[cfg(feature = "run-server")]
419 Commands::Run {
420 input,
421 database,
422 port,
423 bind,
424 watch,
425 introspection,
426 } => {
427 Box::pin(commands::run::run(
431 input.as_deref(),
432 database,
433 port,
434 bind,
435 watch,
436 introspection,
437 ))
438 .await
439 },
440
441 Commands::Watch {
442 input,
443 output,
444 reload_url,
445 admin_token,
446 database,
447 } => {
448 commands::watch::run(
449 &input,
450 &output,
451 reload_url.as_deref(),
452 admin_token.as_deref(),
453 database.as_deref(),
454 )
455 .await
456 },
457
458 Commands::ValidateDocuments { manifest } => {
459 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
460 match commands::validate_documents::run(&manifest, &formatter) {
461 Ok(true) => Ok(()),
462 Ok(false) => {
463 process::exit(2);
464 },
465 Err(e) => Err(e),
466 }
467 },
468
469 Commands::Setup { database, dry_run } => {
470 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
471 commands::setup::run(database.as_deref(), dry_run, &formatter).await
472 },
473
474 Commands::Schema { command } => match command {
475 SchemaCommands::Metadata { server, token } => {
476 commands::schema::metadata::run(&server, token.as_deref()).await
477 },
478 },
479
480 Commands::Query {
481 query,
482 schema,
483 database,
484 variables,
485 dry_run,
486 } => {
487 Box::pin(commands::query::run(&query, &schema, database, variables, dry_run)).await
490 },
491
492 Commands::Doctor {
493 config,
494 schema,
495 db_url,
496 against_db,
497 runtime,
498 schemas,
499 json: json_flag,
500 } => {
501 let all_passed = commands::doctor::run_with_db_checks(
502 &config,
503 &schema,
504 db_url.as_deref(),
505 against_db.as_deref(),
506 runtime,
507 &schemas,
508 json_flag,
509 )
510 .await;
511 if all_passed {
512 Ok(())
513 } else {
514 process::exit(1);
515 }
516 },
517
518 Commands::Perf { command } => match command {
519 PerfCommands::RegressionScan {
520 database,
521 recent_days,
522 baseline_days,
523 min_samples,
524 threshold_pct,
525 min_delta_ms,
526 object_type,
527 fail_on_regression,
528 } => {
529 let args = commands::perf::RegressionScanArgs {
530 database,
531 recent_days,
532 baseline_days,
533 min_samples,
534 threshold_pct,
535 min_delta_ms,
536 object_type,
537 fail_on_regression,
538 json: cli.json,
539 quiet: cli.quiet,
540 };
541 match commands::perf::run_regression_scan(args).await {
542 Ok(true) => Ok(()),
543 Ok(false) => process::exit(1),
544 Err(e) => Err(e),
545 }
546 },
547 PerfCommands::Explore { command } => match command {
548 PerfExploreCommands::Slowest {
549 database,
550 days,
551 object_type,
552 limit,
553 } => {
554 commands::perf::run_explore_slowest(
555 database,
556 days,
557 object_type,
558 limit,
559 cli.json,
560 cli.quiet,
561 )
562 .await
563 },
564 PerfExploreCommands::NullRate {
565 database,
566 days,
567 object_type,
568 } => {
569 commands::perf::run_explore_null_rate(
570 database,
571 days,
572 object_type,
573 cli.json,
574 cli.quiet,
575 )
576 .await
577 },
578 PerfExploreCommands::Summary {
579 database,
580 days,
581 object_type,
582 } => {
583 commands::perf::run_explore_summary(
584 database,
585 days,
586 object_type,
587 cli.json,
588 cli.quiet,
589 )
590 .await
591 },
592 },
593 },
594
595 Commands::Sources {
596 schema,
597 db_url,
598 json,
599 } => {
600 commands::sources::run(commands::sources::SourcesArgs {
601 schema,
602 database: db_url,
603 json,
604 })
605 .await
606 },
607 };
608
609 if let Err(e) = result {
610 let debug_info = if debug_output {
611 Some(format!("{e:?}"))
612 } else {
613 None
614 };
615 let causes = error_causes(&e);
616 eprintln!(
617 "{}",
618 format_cli_error(&e.to_string(), &causes, debug_info.as_deref(), json_output, 1)
619 );
620 process::exit(1);
621 }
622}
623
624fn enforce_exit_code(result: &crate::output::CommandResult) {
637 match result.status.as_str() {
638 "success" => {},
639 "validation-failed" => process::exit(2),
640 _ => process::exit(1),
641 }
642}
643
644pub(crate) fn error_causes(err: &anyhow::Error) -> Vec<String> {
652 err.chain().skip(1).map(ToString::to_string).collect()
653}
654
655pub(crate) fn format_cli_error(
673 message: &str,
674 causes: &[String],
675 debug_info: Option<&str>,
676 json: bool,
677 code: i32,
678) -> String {
679 if json {
680 serde_json::json!({
681 "error": {
682 "message": message,
683 "causes": causes,
684 "code": code
685 }
686 })
687 .to_string()
688 } else {
689 let mut out = format!("Error: {message}");
690 for cause in causes {
691 out.push_str(&format!("\n caused by: {cause}"));
692 }
693 if let Some(debug) = debug_info {
694 out.push_str("\n\nDebug info:\n");
695 out.push_str(debug);
696 }
697 out
698 }
699}
700
701fn init_logging(verbose: bool, debug: bool) {
702 let filter = if debug {
703 "fraiseql=debug,fraiseql_core=debug"
704 } else if verbose {
705 "fraiseql=info,fraiseql_core=info"
706 } else {
707 "fraiseql=warn,fraiseql_core=warn"
708 };
709
710 tracing_subscriber::registry()
711 .with(
712 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
713 )
714 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
718 .init();
719}
720
721fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
723 serde_json::to_value(value).unwrap_or_else(|e| {
724 eprintln!("fraiseql: failed to serialize {context}: {e}");
725 std::process::exit(2);
726 })
727}
728
729fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
731 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
732 eprintln!("fraiseql: failed to format {context}: {e}");
733 std::process::exit(2);
734 })
735}
736
737fn handle_introspection_flags() -> Option<i32> {
738 let args: Vec<String> = env::args().collect();
739
740 if args.iter().any(|a| a == "--help-json") {
741 let cmd = Cli::command();
742 let version = env!("CARGO_PKG_VERSION");
743 let help = crate::introspection::extract_cli_help(&cmd, version);
744 let result =
745 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
746 println!("{}", pretty_or_exit(&result, "command result"));
747 return Some(0);
748 }
749
750 if args.iter().any(|a| a == "--list-commands") {
751 let cmd = Cli::command();
752 let commands = crate::introspection::list_commands(&cmd);
753 let result = crate::output::CommandResult::success(
754 "list-commands",
755 serialize_or_exit(&commands, "command list"),
756 );
757 println!("{}", pretty_or_exit(&result, "command result"));
758 return Some(0);
759 }
760
761 let idx = args.iter().position(|a| a == "--show-output-schema")?;
762 let available = crate::output_schemas::list_schema_commands().join(", ");
763
764 let Some(cmd_name) = args.get(idx + 1) else {
765 let result = crate::output::CommandResult::error(
766 "show-output-schema",
767 &format!("Missing command name. Available: {available}"),
768 "MISSING_ARGUMENT",
769 );
770 println!("{}", pretty_or_exit(&result, "command result"));
771 return Some(1);
772 };
773
774 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
775 let result = crate::output::CommandResult::success(
776 "show-output-schema",
777 serialize_or_exit(&schema, "output schema"),
778 );
779 println!("{}", pretty_or_exit(&result, "command result"));
780 return Some(0);
781 }
782
783 let result = crate::output::CommandResult::error(
784 "show-output-schema",
785 &format!("Unknown command: {cmd_name}. Available: {available}"),
786 "UNKNOWN_COMMAND",
787 );
788 println!("{}", pretty_or_exit(&result, "command result"));
789 Some(1)
790}