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::Auth {
451 command: crate::cli::AuthCommands::RotateSigningKey { input, config },
452 } => {
453 Box::pin(commands::auth::rotate_signing_key(input.as_deref(), config.as_deref())).await
456 },
457
458 #[cfg(feature = "run-server")]
459 Commands::Run {
460 input,
461 database,
462 port,
463 bind,
464 watch,
465 introspection,
466 admin_database_url,
467 } => {
468 Box::pin(commands::run::run(
472 input.as_deref(),
473 database,
474 port,
475 bind,
476 watch,
477 introspection,
478 admin_database_url,
479 ))
480 .await
481 },
482
483 Commands::Watch {
484 input,
485 output,
486 reload_url,
487 admin_token,
488 database,
489 } => {
490 commands::watch::run(
491 &input,
492 &output,
493 reload_url.as_deref(),
494 admin_token.as_deref(),
495 database.as_deref(),
496 )
497 .await
498 },
499
500 Commands::ValidateDocuments {
501 manifest,
502 max_cost,
503 schema,
504 } => {
505 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
506 match commands::validate_documents::run(
507 &manifest,
508 max_cost,
509 schema.as_deref(),
510 &formatter,
511 ) {
512 Ok(true) => Ok(()),
513 Ok(false) => {
514 process::exit(2);
515 },
516 Err(e) => Err(e),
517 }
518 },
519
520 Commands::Setup { database, dry_run } => {
521 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
522 commands::setup::run(database.as_deref(), dry_run, &formatter).await
523 },
524
525 Commands::Schema { command } => match command {
526 SchemaCommands::Metadata { server, token } => {
527 commands::schema::metadata::run(&server, token.as_deref()).await
528 },
529 },
530
531 Commands::Query {
532 query,
533 schema,
534 database,
535 variables,
536 dry_run,
537 } => {
538 Box::pin(commands::query::run(&query, &schema, database, variables, dry_run)).await
541 },
542
543 Commands::Doctor {
544 config,
545 schema,
546 db_url,
547 against_db,
548 runtime,
549 schemas,
550 json: json_flag,
551 } => {
552 let all_passed = commands::doctor::run_with_db_checks(
553 &config,
554 &schema,
555 db_url.as_deref(),
556 against_db.as_deref(),
557 runtime,
558 &schemas,
559 json_flag,
560 )
561 .await;
562 if all_passed {
563 Ok(())
564 } else {
565 process::exit(1);
566 }
567 },
568
569 Commands::Perf { command } => match command {
570 PerfCommands::RegressionScan {
571 database,
572 recent_days,
573 baseline_days,
574 min_samples,
575 threshold_pct,
576 min_delta_ms,
577 object_type,
578 fail_on_regression,
579 } => {
580 let args = commands::perf::RegressionScanArgs {
581 database,
582 recent_days,
583 baseline_days,
584 min_samples,
585 threshold_pct,
586 min_delta_ms,
587 object_type,
588 fail_on_regression,
589 json: cli.json,
590 quiet: cli.quiet,
591 };
592 match commands::perf::run_regression_scan(args).await {
593 Ok(true) => Ok(()),
594 Ok(false) => process::exit(1),
595 Err(e) => Err(e),
596 }
597 },
598 PerfCommands::Explore { command } => match command {
599 PerfExploreCommands::Slowest {
600 database,
601 days,
602 object_type,
603 limit,
604 } => {
605 commands::perf::run_explore_slowest(
606 database,
607 days,
608 object_type,
609 limit,
610 cli.json,
611 cli.quiet,
612 )
613 .await
614 },
615 PerfExploreCommands::NullRate {
616 database,
617 days,
618 object_type,
619 } => {
620 commands::perf::run_explore_null_rate(
621 database,
622 days,
623 object_type,
624 cli.json,
625 cli.quiet,
626 )
627 .await
628 },
629 PerfExploreCommands::Summary {
630 database,
631 days,
632 object_type,
633 } => {
634 commands::perf::run_explore_summary(
635 database,
636 days,
637 object_type,
638 cli.json,
639 cli.quiet,
640 )
641 .await
642 },
643 },
644 },
645
646 Commands::Sources {
647 schema,
648 db_url,
649 json,
650 } => {
651 commands::sources::run(commands::sources::SourcesArgs {
652 schema,
653 database: db_url,
654 json,
655 })
656 .await
657 },
658
659 #[cfg(feature = "functions-invoke")]
660 Commands::Functions(command) => match command {
661 crate::cli::FunctionsCommands::Invoke {
662 name,
663 payload,
664 schema,
665 mock_http,
666 mock_query,
667 idempotency_token,
668 explain,
669 } => match commands::functions::invoke(
670 &name,
671 &payload,
672 &schema,
673 mock_http.as_deref(),
674 mock_query.as_deref(),
675 idempotency_token,
676 explain,
677 cli.json,
678 )
679 .await
680 {
681 Ok(code) => process::exit(code),
683 Err(error) => Err(error),
685 },
686 },
687 };
688
689 if let Err(e) = result {
690 let debug_info = if debug_output {
691 Some(format!("{e:?}"))
692 } else {
693 None
694 };
695 let causes = error_causes(&e);
696 eprintln!(
697 "{}",
698 format_cli_error(&e.to_string(), &causes, debug_info.as_deref(), json_output, 1)
699 );
700 process::exit(1);
701 }
702}
703
704fn enforce_exit_code(result: &crate::output::CommandResult) {
717 match result.status.as_str() {
718 "success" => {},
719 "validation-failed" => process::exit(2),
720 _ => process::exit(1),
721 }
722}
723
724pub(crate) fn error_causes(err: &anyhow::Error) -> Vec<String> {
732 err.chain().skip(1).map(ToString::to_string).collect()
733}
734
735pub(crate) fn format_cli_error(
753 message: &str,
754 causes: &[String],
755 debug_info: Option<&str>,
756 json: bool,
757 code: i32,
758) -> String {
759 if json {
760 serde_json::json!({
761 "error": {
762 "message": message,
763 "causes": causes,
764 "code": code
765 }
766 })
767 .to_string()
768 } else {
769 let mut out = format!("Error: {message}");
770 for cause in causes {
771 out.push_str(&format!("\n caused by: {cause}"));
772 }
773 if let Some(debug) = debug_info {
774 out.push_str("\n\nDebug info:\n");
775 out.push_str(debug);
776 }
777 out
778 }
779}
780
781fn init_logging(verbose: bool, debug: bool) {
782 let filter = if debug {
783 "fraiseql=debug,fraiseql_core=debug"
784 } else if verbose {
785 "fraiseql=info,fraiseql_core=info"
786 } else {
787 "fraiseql=warn,fraiseql_core=warn"
788 };
789
790 tracing_subscriber::registry()
791 .with(
792 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
793 )
794 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
798 .init();
799}
800
801fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
803 serde_json::to_value(value).unwrap_or_else(|e| {
804 eprintln!("fraiseql: failed to serialize {context}: {e}");
805 std::process::exit(2);
806 })
807}
808
809fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
811 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
812 eprintln!("fraiseql: failed to format {context}: {e}");
813 std::process::exit(2);
814 })
815}
816
817fn handle_introspection_flags() -> Option<i32> {
818 let args: Vec<String> = env::args().collect();
819
820 if args.iter().any(|a| a == "--help-json") {
821 let cmd = Cli::command();
822 let version = env!("CARGO_PKG_VERSION");
823 let help = crate::introspection::extract_cli_help(&cmd, version);
824 let result =
825 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
826 println!("{}", pretty_or_exit(&result, "command result"));
827 return Some(0);
828 }
829
830 if args.iter().any(|a| a == "--list-commands") {
831 let cmd = Cli::command();
832 let commands = crate::introspection::list_commands(&cmd);
833 let result = crate::output::CommandResult::success(
834 "list-commands",
835 serialize_or_exit(&commands, "command list"),
836 );
837 println!("{}", pretty_or_exit(&result, "command result"));
838 return Some(0);
839 }
840
841 let idx = args.iter().position(|a| a == "--show-output-schema")?;
842 let available = crate::output_schemas::list_schema_commands().join(", ");
843
844 let Some(cmd_name) = args.get(idx + 1) else {
845 let result = crate::output::CommandResult::error(
846 "show-output-schema",
847 &format!("Missing command name. Available: {available}"),
848 "MISSING_ARGUMENT",
849 );
850 println!("{}", pretty_or_exit(&result, "command result"));
851 return Some(1);
852 };
853
854 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
855 let result = crate::output::CommandResult::success(
856 "show-output-schema",
857 serialize_or_exit(&schema, "output schema"),
858 );
859 println!("{}", pretty_or_exit(&result, "command result"));
860 return Some(0);
861 }
862
863 let result = crate::output::CommandResult::error(
864 "show-output-schema",
865 &format!("Unknown command: {cmd_name}. Available: {available}"),
866 "UNKNOWN_COMMAND",
867 );
868 println!("{}", pretty_or_exit(&result, "command result"));
869 Some(1)
870}
871
872async fn migrate_database(
875 explicit: Option<&str>,
876 formatter: &crate::output::OutputFormatter,
877) -> anyhow::Result<String> {
878 crate::commands::migrate::require_confiture(formatter)?;
879 let database = crate::commands::migrate::resolve_database(explicit)?;
880 crate::commands::migrate::refuse_unsupported_server(&database).await?;
881 Ok(database.url)
882}