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 allow_drift,
44 config,
45 } => {
46 commands::compile::run(
47 &input,
48 types.as_deref(),
49 schema_dir.as_deref(),
50 type_files,
51 query_files,
52 mutation_files,
53 &output,
54 check,
55 database.as_deref(),
56 emit_ddl.as_deref(),
57 check_migrations,
58 skip_hash,
59 allow_drift,
60 config.as_deref(),
61 )
62 .await
63 },
64
65 Commands::Extract {
66 input,
67 language,
68 recursive,
69 output,
70 } => commands::extract::run(&input, language.as_deref(), recursive, &output),
71
72 Commands::Explain { query } => match commands::explain::run(&query) {
73 Ok(result) => {
74 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
75 Ok(())
76 },
77 Err(e) => Err(e),
78 },
79
80 Commands::Cost { query } => match commands::cost::run(&query) {
81 Ok(result) => {
82 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
83 Ok(())
84 },
85 Err(e) => Err(e),
86 },
87
88 Commands::Analyze { schema } => match commands::analyze::run(&schema) {
89 Ok(result) => {
90 println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
91 Ok(())
92 },
93 Err(e) => Err(e),
94 },
95
96 Commands::DependencyGraph { schema, format } => {
97 match commands::dependency_graph::GraphFormat::from_str(&format) {
98 Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
99 Ok(result) => {
100 println!(
101 "{}",
102 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
103 );
104 Ok(())
105 },
106 Err(e) => Err(e),
107 },
108 Err(e) => Err(anyhow::anyhow!(e)),
109 }
110 },
111
112 Commands::Lint {
113 schema,
114 federation,
115 cost,
116 cache,
117 auth,
118 compilation,
119 fail_on_critical,
120 fail_on_warning,
121 } => {
122 let opts = commands::lint::LintOptions {
123 fail_on_critical,
124 fail_on_warning,
125 filter: commands::lint::LintCategoryFilter {
126 federation,
127 cost,
128 cache,
129 auth,
130 compilation,
131 },
132 };
133 match commands::lint::run(&schema, opts) {
134 Ok(result) => {
135 println!(
136 "{}",
137 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
138 );
139 enforce_exit_code(&result);
142 Ok(())
143 },
144 Err(e) => Err(e),
145 }
146 },
147
148 Commands::Federation { command } => match command {
149 FederationCommands::Graph { schema, format } => {
150 match commands::federation::graph::GraphFormat::from_str(&format) {
151 Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
152 Ok(result) => {
153 println!(
154 "{}",
155 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
156 );
157 Ok(())
158 },
159 Err(e) => Err(e),
160 },
161 Err(e) => Err(anyhow::anyhow!(e)),
162 }
163 },
164 #[cfg(feature = "federation")]
165 FederationCommands::Sdl { schema, output } => {
166 commands::federation::sdl::run(&schema, output.as_deref())
167 },
168 FederationCommands::Check {
169 schema,
170 against,
171 json,
172 } => {
173 match commands::federation::check::run(
174 &schema,
175 against.as_deref(),
176 json || cli.json,
177 ) {
178 Ok(result) => {
179 if !(json || cli.json) {
184 println!(
185 "{}",
186 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
187 );
188 }
189 enforce_exit_code(&result);
192 Ok(())
193 },
194 Err(e) => Err(e),
195 }
196 },
197 },
198
199 Commands::GenerateViews {
200 schema,
201 entity,
202 view,
203 refresh_strategy,
204 output,
205 include_composition_views,
206 include_monitoring,
207 validate,
208 gen_verbose,
209 } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
210 Ok(refresh_strat) => {
211 let config = commands::generate_views::GenerateViewsConfig {
212 schema_path: schema,
213 entity,
214 view,
215 refresh_strategy: refresh_strat,
216 output,
217 include_composition_views,
218 include_monitoring,
219 validate_only: validate,
220 verbose: cli.verbose || gen_verbose,
221 };
222
223 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
224 commands::generate_views::run(config, &formatter).await
225 },
226 Err(e) => Err(anyhow::anyhow!(e)),
227 },
228
229 Commands::GenerateCaptureTriggers {
230 schema,
231 output,
232 no_function,
233 } => {
234 let config = commands::generate_capture_triggers::GenerateCaptureTriggersConfig {
235 schema_path: schema,
236 output,
237 include_function: !no_function,
238 };
239 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
240 commands::generate_capture_triggers::run(config, &formatter)
241 },
242
243 Commands::Validate {
244 command,
245 input,
246 check_cycles,
247 check_unused,
248 strict,
249 types,
250 against_db,
251 } => match command {
252 Some(ValidateCommands::Facts { schema, database }) => {
253 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
254 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
255 .await
256 },
257 None => match (against_db, input) {
258 (Some(db_url), Some(input)) => {
259 commands::validate::run_against_db(&input, &db_url, cli.json).await
260 },
261 (Some(_), None) => Err(anyhow::anyhow!(
262 "INPUT (schema.compiled.json) is required with --against-db"
263 )),
264 (None, Some(input)) => {
265 let opts = commands::validate::ValidateOptions {
266 check_cycles,
267 check_unused,
268 strict,
269 filter_types: types,
270 };
271 match commands::validate::run_with_options(&input, opts) {
272 Ok(result) => {
273 println!(
274 "{}",
275 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
276 );
277 enforce_exit_code(&result);
286 Ok(())
287 },
288 Err(e) => Err(e),
289 }
290 },
291 (None, None) => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
292 },
293 },
294
295 Commands::Introspect { command } => match command {
296 IntrospectCommands::Facts { database, format } => {
297 match commands::introspect_facts::OutputFormat::parse(&format) {
298 Ok(fmt) => {
299 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
300 commands::introspect_facts::run(&database, fmt, &formatter).await
301 },
302 Err(e) => Err(anyhow::anyhow!(e)),
303 }
304 },
305 },
306
307 Commands::Generate {
308 input,
309 language,
310 output,
311 } => match commands::init::Language::from_str(&language) {
312 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
313 Err(e) => Err(anyhow::anyhow!(e)),
314 },
315
316 Commands::GenerateClient { language } => match language {
317 GenerateClientCommands::Typescript { schema, out, force } => {
318 commands::generate_client::run(
319 commands::generate_client::ClientLanguage::TypeScript,
320 schema.as_deref(),
321 &out,
322 force,
323 )
324 },
325 GenerateClientCommands::Python { schema, out, force } => {
326 commands::generate_client::run(
327 commands::generate_client::ClientLanguage::Python,
328 schema.as_deref(),
329 &out,
330 force,
331 )
332 },
333 GenerateClientCommands::Go { schema, out, force } => commands::generate_client::run(
334 commands::generate_client::ClientLanguage::Go,
335 schema.as_deref(),
336 &out,
337 force,
338 ),
339 GenerateClientCommands::Rust { schema, out, force } => commands::generate_client::run(
340 commands::generate_client::ClientLanguage::Rust,
341 schema.as_deref(),
342 &out,
343 force,
344 ),
345 },
346
347 Commands::Init {
348 project_name,
349 language,
350 database,
351 size,
352 no_git,
353 } => {
354 match (
355 commands::init::Language::from_str(&language),
356 commands::init::Database::from_str(&database),
357 commands::init::ProjectSize::from_str(&size),
358 ) {
359 (Ok(lang), Ok(db), Ok(sz)) => {
360 let config = commands::init::InitConfig {
361 project_name,
362 language: lang,
363 database: db,
364 size: sz,
365 no_git,
366 };
367 commands::init::run(&config)
368 },
369 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
370 }
371 },
372
373 Commands::Migrate { command } => {
374 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
375 match command {
376 MigrateCommands::Up { database, dir } => {
377 match migrate_database(database.as_deref(), &formatter).await {
378 Ok(url) => {
379 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
380 let action = commands::migrate::MigrateAction::Up {
381 database_url: url,
382 dir: mig_dir,
383 };
384 commands::migrate::run(&action, &formatter)
385 },
386 Err(e) => Err(e),
387 }
388 },
389 MigrateCommands::Down {
390 database,
391 dir,
392 steps,
393 } => match migrate_database(database.as_deref(), &formatter).await {
394 Ok(url) => {
395 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
396 let action = commands::migrate::MigrateAction::Down {
397 database_url: url,
398 dir: mig_dir,
399 steps,
400 };
401 commands::migrate::run(&action, &formatter)
402 },
403 Err(e) => Err(e),
404 },
405 MigrateCommands::Status { database, dir } => {
406 match migrate_database(database.as_deref(), &formatter).await {
407 Ok(url) => {
408 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
409 let action = commands::migrate::MigrateAction::Status {
410 database_url: url,
411 dir: mig_dir,
412 };
413 commands::migrate::run(&action, &formatter)
414 },
415 Err(e) => Err(e),
416 }
417 },
418 MigrateCommands::Create { name, dir } => {
419 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
420 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
421 commands::migrate::run(&action, &formatter)
422 },
423 MigrateCommands::Generate { name, dir } => {
424 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
425 let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
426 commands::migrate::run(&action, &formatter)
427 },
428 MigrateCommands::Validate { dir } => {
429 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
430 let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
431 commands::migrate::run(&action, &formatter)
432 },
433 MigrateCommands::Preflight { dir } => {
434 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
435 let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
436 commands::migrate::run(&action, &formatter)
437 },
438 }
439 },
440
441 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
442 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
443 Err(e) => Err(anyhow::anyhow!(e)),
444 },
445
446 #[cfg(feature = "run-server")]
447 Commands::Capabilities => commands::capabilities::run(cli.json, cli.quiet),
448
449 #[cfg(feature = "run-server")]
450 Commands::Run {
451 input,
452 database,
453 port,
454 bind,
455 watch,
456 introspection,
457 } => {
458 Box::pin(commands::run::run(
462 input.as_deref(),
463 database,
464 port,
465 bind,
466 watch,
467 introspection,
468 ))
469 .await
470 },
471
472 Commands::Watch {
473 input,
474 output,
475 reload_url,
476 admin_token,
477 database,
478 } => {
479 commands::watch::run(
480 &input,
481 &output,
482 reload_url.as_deref(),
483 admin_token.as_deref(),
484 database.as_deref(),
485 )
486 .await
487 },
488
489 Commands::ValidateDocuments {
490 manifest,
491 max_cost,
492 schema,
493 } => {
494 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
495 match commands::validate_documents::run(
496 &manifest,
497 max_cost,
498 schema.as_deref(),
499 &formatter,
500 ) {
501 Ok(true) => Ok(()),
502 Ok(false) => {
503 process::exit(2);
504 },
505 Err(e) => Err(e),
506 }
507 },
508
509 Commands::Setup { database, dry_run } => {
510 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
511 commands::setup::run(database.as_deref(), dry_run, &formatter).await
512 },
513
514 Commands::Schema { command } => match command {
515 SchemaCommands::Metadata { server, token } => {
516 commands::schema::metadata::run(&server, token.as_deref()).await
517 },
518 },
519
520 Commands::Query {
521 query,
522 schema,
523 database,
524 variables,
525 dry_run,
526 } => {
527 Box::pin(commands::query::run(&query, &schema, database, variables, dry_run)).await
530 },
531
532 Commands::Doctor {
533 config,
534 schema,
535 db_url,
536 against_db,
537 runtime,
538 schemas,
539 json: json_flag,
540 } => {
541 let all_passed = commands::doctor::run_with_db_checks(
542 &config,
543 &schema,
544 db_url.as_deref(),
545 against_db.as_deref(),
546 runtime,
547 &schemas,
548 json_flag,
549 )
550 .await;
551 if all_passed {
552 Ok(())
553 } else {
554 process::exit(1);
555 }
556 },
557
558 Commands::Perf { command } => match command {
559 PerfCommands::RegressionScan {
560 database,
561 recent_days,
562 baseline_days,
563 min_samples,
564 threshold_pct,
565 min_delta_ms,
566 object_type,
567 fail_on_regression,
568 } => {
569 let args = commands::perf::RegressionScanArgs {
570 database,
571 recent_days,
572 baseline_days,
573 min_samples,
574 threshold_pct,
575 min_delta_ms,
576 object_type,
577 fail_on_regression,
578 json: cli.json,
579 quiet: cli.quiet,
580 };
581 match commands::perf::run_regression_scan(args).await {
582 Ok(true) => Ok(()),
583 Ok(false) => process::exit(1),
584 Err(e) => Err(e),
585 }
586 },
587 PerfCommands::Explore { command } => match command {
588 PerfExploreCommands::Slowest {
589 database,
590 days,
591 object_type,
592 limit,
593 } => {
594 commands::perf::run_explore_slowest(
595 database,
596 days,
597 object_type,
598 limit,
599 cli.json,
600 cli.quiet,
601 )
602 .await
603 },
604 PerfExploreCommands::NullRate {
605 database,
606 days,
607 object_type,
608 } => {
609 commands::perf::run_explore_null_rate(
610 database,
611 days,
612 object_type,
613 cli.json,
614 cli.quiet,
615 )
616 .await
617 },
618 PerfExploreCommands::Summary {
619 database,
620 days,
621 object_type,
622 } => {
623 commands::perf::run_explore_summary(
624 database,
625 days,
626 object_type,
627 cli.json,
628 cli.quiet,
629 )
630 .await
631 },
632 },
633 },
634
635 Commands::Sources {
636 schema,
637 db_url,
638 json,
639 } => {
640 commands::sources::run(commands::sources::SourcesArgs {
641 schema,
642 database: db_url,
643 json,
644 })
645 .await
646 },
647
648 #[cfg(feature = "functions-invoke")]
649 Commands::Functions(command) => match command {
650 crate::cli::FunctionsCommands::Invoke {
651 name,
652 payload,
653 schema,
654 mock_http,
655 mock_query,
656 idempotency_token,
657 explain,
658 } => match commands::functions::invoke(
659 &name,
660 &payload,
661 &schema,
662 mock_http.as_deref(),
663 mock_query.as_deref(),
664 idempotency_token,
665 explain,
666 cli.json,
667 )
668 .await
669 {
670 Ok(code) => process::exit(code),
672 Err(error) => Err(error),
674 },
675 },
676 };
677
678 if let Err(e) = result {
679 let debug_info = if debug_output {
680 Some(format!("{e:?}"))
681 } else {
682 None
683 };
684 let causes = error_causes(&e);
685 eprintln!(
686 "{}",
687 format_cli_error(&e.to_string(), &causes, debug_info.as_deref(), json_output, 1)
688 );
689 process::exit(1);
690 }
691}
692
693fn enforce_exit_code(result: &crate::output::CommandResult) {
706 match result.status.as_str() {
707 "success" => {},
708 "validation-failed" => process::exit(2),
709 _ => process::exit(1),
710 }
711}
712
713pub(crate) fn error_causes(err: &anyhow::Error) -> Vec<String> {
721 err.chain().skip(1).map(ToString::to_string).collect()
722}
723
724pub(crate) fn format_cli_error(
742 message: &str,
743 causes: &[String],
744 debug_info: Option<&str>,
745 json: bool,
746 code: i32,
747) -> String {
748 if json {
749 serde_json::json!({
750 "error": {
751 "message": message,
752 "causes": causes,
753 "code": code
754 }
755 })
756 .to_string()
757 } else {
758 let mut out = format!("Error: {message}");
759 for cause in causes {
760 out.push_str(&format!("\n caused by: {cause}"));
761 }
762 if let Some(debug) = debug_info {
763 out.push_str("\n\nDebug info:\n");
764 out.push_str(debug);
765 }
766 out
767 }
768}
769
770fn init_logging(verbose: bool, debug: bool) {
771 let filter = if debug {
772 "fraiseql=debug,fraiseql_core=debug"
773 } else if verbose {
774 "fraiseql=info,fraiseql_core=info"
775 } else {
776 "fraiseql=warn,fraiseql_core=warn"
777 };
778
779 tracing_subscriber::registry()
780 .with(
781 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
782 )
783 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
787 .init();
788}
789
790fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
792 serde_json::to_value(value).unwrap_or_else(|e| {
793 eprintln!("fraiseql: failed to serialize {context}: {e}");
794 std::process::exit(2);
795 })
796}
797
798fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
800 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
801 eprintln!("fraiseql: failed to format {context}: {e}");
802 std::process::exit(2);
803 })
804}
805
806fn handle_introspection_flags() -> Option<i32> {
807 let args: Vec<String> = env::args().collect();
808
809 if args.iter().any(|a| a == "--help-json") {
810 let cmd = Cli::command();
811 let version = env!("CARGO_PKG_VERSION");
812 let help = crate::introspection::extract_cli_help(&cmd, version);
813 let result =
814 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
815 println!("{}", pretty_or_exit(&result, "command result"));
816 return Some(0);
817 }
818
819 if args.iter().any(|a| a == "--list-commands") {
820 let cmd = Cli::command();
821 let commands = crate::introspection::list_commands(&cmd);
822 let result = crate::output::CommandResult::success(
823 "list-commands",
824 serialize_or_exit(&commands, "command list"),
825 );
826 println!("{}", pretty_or_exit(&result, "command result"));
827 return Some(0);
828 }
829
830 let idx = args.iter().position(|a| a == "--show-output-schema")?;
831 let available = crate::output_schemas::list_schema_commands().join(", ");
832
833 let Some(cmd_name) = args.get(idx + 1) else {
834 let result = crate::output::CommandResult::error(
835 "show-output-schema",
836 &format!("Missing command name. Available: {available}"),
837 "MISSING_ARGUMENT",
838 );
839 println!("{}", pretty_or_exit(&result, "command result"));
840 return Some(1);
841 };
842
843 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
844 let result = crate::output::CommandResult::success(
845 "show-output-schema",
846 serialize_or_exit(&schema, "output schema"),
847 );
848 println!("{}", pretty_or_exit(&result, "command result"));
849 return Some(0);
850 }
851
852 let result = crate::output::CommandResult::error(
853 "show-output-schema",
854 &format!("Unknown command: {cmd_name}. Available: {available}"),
855 "UNKNOWN_COMMAND",
856 );
857 println!("{}", pretty_or_exit(&result, "command result"));
858 Some(1)
859}
860
861async fn migrate_database(
864 explicit: Option<&str>,
865 formatter: &crate::output::OutputFormatter,
866) -> anyhow::Result<String> {
867 crate::commands::migrate::require_confiture(formatter)?;
868 let database = crate::commands::migrate::resolve_database(explicit)?;
869 crate::commands::migrate::refuse_unsupported_server(&database).await?;
870 Ok(database.url)
871}