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