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 verbose: _,
118 } => {
119 let opts = commands::lint::LintOptions {
120 fail_on_critical,
121 fail_on_warning,
122 filter: commands::lint::LintCategoryFilter {
123 federation,
124 cost,
125 cache,
126 auth,
127 compilation,
128 },
129 };
130 match commands::lint::run(&schema, opts) {
131 Ok(result) => {
132 println!(
133 "{}",
134 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
135 );
136 enforce_exit_code(&result);
139 Ok(())
140 },
141 Err(e) => Err(e),
142 }
143 },
144
145 Commands::Federation { command } => match command {
146 FederationCommands::Graph { schema, format } => {
147 match commands::federation::graph::GraphFormat::from_str(&format) {
148 Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
149 Ok(result) => {
150 println!(
151 "{}",
152 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
153 );
154 Ok(())
155 },
156 Err(e) => Err(e),
157 },
158 Err(e) => Err(anyhow::anyhow!(e)),
159 }
160 },
161 FederationCommands::Check {
162 schema,
163 against,
164 json,
165 } => {
166 match commands::federation::check::run(
167 &schema,
168 against.as_deref(),
169 json || cli.json,
170 ) {
171 Ok(result) => {
172 if !json {
173 println!(
174 "{}",
175 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
176 );
177 }
178 enforce_exit_code(&result);
181 Ok(())
182 },
183 Err(e) => Err(e),
184 }
185 },
186 },
187
188 Commands::GenerateViews {
189 schema,
190 entity,
191 view,
192 refresh_strategy,
193 output,
194 include_composition_views,
195 include_monitoring,
196 validate,
197 gen_verbose,
198 } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
199 Ok(refresh_strat) => {
200 let config = commands::generate_views::GenerateViewsConfig {
201 schema_path: schema,
202 entity,
203 view,
204 refresh_strategy: refresh_strat,
205 output,
206 include_composition_views,
207 include_monitoring,
208 validate_only: validate,
209 verbose: cli.verbose || gen_verbose,
210 };
211
212 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
213 commands::generate_views::run(config, &formatter)
214 },
215 Err(e) => Err(anyhow::anyhow!(e)),
216 },
217
218 Commands::GenerateCaptureTriggers {
219 schema,
220 output,
221 no_function,
222 } => {
223 let config = commands::generate_capture_triggers::GenerateCaptureTriggersConfig {
224 schema_path: schema,
225 output,
226 include_function: !no_function,
227 };
228 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
229 commands::generate_capture_triggers::run(config, &formatter)
230 },
231
232 Commands::Validate {
233 command,
234 input,
235 check_cycles,
236 check_unused,
237 strict,
238 types,
239 against_db,
240 } => match command {
241 Some(ValidateCommands::Facts { schema, database }) => {
242 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
243 commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
244 .await
245 },
246 None => match (against_db, input) {
247 (Some(db_url), Some(input)) => {
248 commands::validate::run_against_db(&input, &db_url, cli.json).await
249 },
250 (Some(_), None) => Err(anyhow::anyhow!(
251 "INPUT (schema.compiled.json) is required with --against-db"
252 )),
253 (None, Some(input)) => {
254 let opts = commands::validate::ValidateOptions {
255 check_cycles,
256 check_unused,
257 strict,
258 filter_types: types,
259 };
260 match commands::validate::run_with_options(&input, opts) {
261 Ok(result) => {
262 println!(
263 "{}",
264 output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
265 );
266 if result.status == "validation-failed" {
267 Err(anyhow::anyhow!("Validation failed"))
268 } else {
269 Ok(())
270 }
271 },
272 Err(e) => Err(e),
273 }
274 },
275 (None, None) => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
276 },
277 },
278
279 Commands::Introspect { command } => match command {
280 IntrospectCommands::Facts { database, format } => {
281 match commands::introspect_facts::OutputFormat::parse(&format) {
282 Ok(fmt) => {
283 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
284 commands::introspect_facts::run(&database, fmt, &formatter).await
285 },
286 Err(e) => Err(anyhow::anyhow!(e)),
287 }
288 },
289 },
290
291 Commands::Generate {
292 input,
293 language,
294 output,
295 } => match commands::init::Language::from_str(&language) {
296 Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
297 Err(e) => Err(anyhow::anyhow!(e)),
298 },
299
300 Commands::GenerateClient { language } => match language {
301 GenerateClientCommands::Typescript { schema, out, force } => {
302 commands::generate_client::run(
303 commands::generate_client::ClientLanguage::TypeScript,
304 schema.as_deref(),
305 &out,
306 force,
307 )
308 },
309 },
310
311 Commands::Init {
312 project_name,
313 language,
314 database,
315 size,
316 no_git,
317 } => {
318 match (
319 commands::init::Language::from_str(&language),
320 commands::init::Database::from_str(&database),
321 commands::init::ProjectSize::from_str(&size),
322 ) {
323 (Ok(lang), Ok(db), Ok(sz)) => {
324 let config = commands::init::InitConfig {
325 project_name,
326 language: lang,
327 database: db,
328 size: sz,
329 no_git,
330 };
331 commands::init::run(&config)
332 },
333 (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
334 }
335 },
336
337 Commands::Migrate { command } => {
338 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
339 match command {
340 MigrateCommands::Up { database, dir } => {
341 let db_url = commands::migrate::resolve_database_url(database.as_deref());
342 match db_url {
343 Ok(url) => {
344 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
345 let action = commands::migrate::MigrateAction::Up {
346 database_url: url,
347 dir: mig_dir,
348 };
349 commands::migrate::run(&action, &formatter)
350 },
351 Err(e) => Err(e),
352 }
353 },
354 MigrateCommands::Down {
355 database,
356 dir,
357 steps,
358 } => {
359 let db_url = commands::migrate::resolve_database_url(database.as_deref());
360 match db_url {
361 Ok(url) => {
362 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
363 let action = commands::migrate::MigrateAction::Down {
364 database_url: url,
365 dir: mig_dir,
366 steps,
367 };
368 commands::migrate::run(&action, &formatter)
369 },
370 Err(e) => Err(e),
371 }
372 },
373 MigrateCommands::Status { database, dir } => {
374 let db_url = commands::migrate::resolve_database_url(database.as_deref());
375 match db_url {
376 Ok(url) => {
377 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
378 let action = commands::migrate::MigrateAction::Status {
379 database_url: url,
380 dir: mig_dir,
381 };
382 commands::migrate::run(&action, &formatter)
383 },
384 Err(e) => Err(e),
385 }
386 },
387 MigrateCommands::Create { name, dir } => {
388 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
389 let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
390 commands::migrate::run(&action, &formatter)
391 },
392 MigrateCommands::Generate { name, dir } => {
393 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
394 let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
395 commands::migrate::run(&action, &formatter)
396 },
397 MigrateCommands::Validate { dir } => {
398 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
399 let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
400 commands::migrate::run(&action, &formatter)
401 },
402 MigrateCommands::Preflight { dir } => {
403 let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
404 let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
405 commands::migrate::run(&action, &formatter)
406 },
407 }
408 },
409
410 Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
411 Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
412 Err(e) => Err(anyhow::anyhow!(e)),
413 },
414
415 #[cfg(feature = "run-server")]
416 Commands::Run {
417 input,
418 database,
419 port,
420 bind,
421 watch,
422 introspection,
423 } => {
424 Box::pin(commands::run::run(
428 input.as_deref(),
429 database,
430 port,
431 bind,
432 watch,
433 introspection,
434 ))
435 .await
436 },
437
438 Commands::ValidateDocuments { manifest } => {
439 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
440 match commands::validate_documents::run(&manifest, &formatter) {
441 Ok(true) => Ok(()),
442 Ok(false) => {
443 process::exit(2);
444 },
445 Err(e) => Err(e),
446 }
447 },
448
449 Commands::Setup { database, dry_run } => {
450 let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
451 commands::setup::run(database.as_deref(), dry_run, &formatter).await
452 },
453
454 Commands::Schema { command } => match command {
455 SchemaCommands::Metadata { server, token } => {
456 commands::schema::metadata::run(&server, token.as_deref()).await
457 },
458 },
459
460 Commands::Doctor {
461 config,
462 schema,
463 db_url,
464 against_db,
465 schemas,
466 json: json_flag,
467 } => {
468 let all_passed = commands::doctor::run_with_db_checks(
469 &config,
470 &schema,
471 db_url.as_deref(),
472 against_db.as_deref(),
473 &schemas,
474 json_flag,
475 )
476 .await;
477 if all_passed {
478 Ok(())
479 } else {
480 process::exit(1);
481 }
482 },
483
484 Commands::Perf { command } => match command {
485 PerfCommands::RegressionScan {
486 database,
487 recent_days,
488 baseline_days,
489 min_samples,
490 threshold_pct,
491 min_delta_ms,
492 object_type,
493 fail_on_regression,
494 } => {
495 let args = commands::perf::RegressionScanArgs {
496 database,
497 recent_days,
498 baseline_days,
499 min_samples,
500 threshold_pct,
501 min_delta_ms,
502 object_type,
503 fail_on_regression,
504 json: cli.json,
505 quiet: cli.quiet,
506 };
507 match commands::perf::run_regression_scan(args).await {
508 Ok(true) => Ok(()),
509 Ok(false) => process::exit(1),
510 Err(e) => Err(e),
511 }
512 },
513 PerfCommands::Explore { command } => match command {
514 PerfExploreCommands::Slowest {
515 database,
516 days,
517 object_type,
518 limit,
519 } => {
520 commands::perf::run_explore_slowest(
521 database,
522 days,
523 object_type,
524 limit,
525 cli.json,
526 cli.quiet,
527 )
528 .await
529 },
530 PerfExploreCommands::NullRate {
531 database,
532 days,
533 object_type,
534 } => {
535 commands::perf::run_explore_null_rate(
536 database,
537 days,
538 object_type,
539 cli.json,
540 cli.quiet,
541 )
542 .await
543 },
544 PerfExploreCommands::Summary {
545 database,
546 days,
547 object_type,
548 } => {
549 commands::perf::run_explore_summary(
550 database,
551 days,
552 object_type,
553 cli.json,
554 cli.quiet,
555 )
556 .await
557 },
558 },
559 },
560 };
561
562 if let Err(e) = result {
563 let debug_info = if debug_output {
564 Some(format!("{e:?}"))
565 } else {
566 None
567 };
568 eprintln!("{}", format_cli_error(&e.to_string(), debug_info.as_deref(), json_output, 1));
569 process::exit(1);
570 }
571}
572
573fn enforce_exit_code(result: &crate::output::CommandResult) {
586 match result.status.as_str() {
587 "success" => {},
588 "validation-failed" => process::exit(2),
589 _ => process::exit(1),
590 }
591}
592
593pub(crate) fn format_cli_error(
605 message: &str,
606 debug_info: Option<&str>,
607 json: bool,
608 code: i32,
609) -> String {
610 if json {
611 serde_json::json!({
612 "error": {
613 "message": message,
614 "code": code
615 }
616 })
617 .to_string()
618 } else {
619 let mut out = format!("Error: {message}");
620 if let Some(debug) = debug_info {
621 out.push_str("\n\nDebug info:\n");
622 out.push_str(debug);
623 }
624 out
625 }
626}
627
628fn init_logging(verbose: bool, debug: bool) {
629 let filter = if debug {
630 "fraiseql=debug,fraiseql_core=debug"
631 } else if verbose {
632 "fraiseql=info,fraiseql_core=info"
633 } else {
634 "fraiseql=warn,fraiseql_core=warn"
635 };
636
637 tracing_subscriber::registry()
638 .with(
639 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
640 )
641 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
645 .init();
646}
647
648fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
650 serde_json::to_value(value).unwrap_or_else(|e| {
651 eprintln!("fraiseql: failed to serialize {context}: {e}");
652 std::process::exit(2);
653 })
654}
655
656fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
658 serde_json::to_string_pretty(value).unwrap_or_else(|e| {
659 eprintln!("fraiseql: failed to format {context}: {e}");
660 std::process::exit(2);
661 })
662}
663
664fn handle_introspection_flags() -> Option<i32> {
665 let args: Vec<String> = env::args().collect();
666
667 if args.iter().any(|a| a == "--help-json") {
668 let cmd = Cli::command();
669 let version = env!("CARGO_PKG_VERSION");
670 let help = crate::introspection::extract_cli_help(&cmd, version);
671 let result =
672 crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
673 println!("{}", pretty_or_exit(&result, "command result"));
674 return Some(0);
675 }
676
677 if args.iter().any(|a| a == "--list-commands") {
678 let cmd = Cli::command();
679 let commands = crate::introspection::list_commands(&cmd);
680 let result = crate::output::CommandResult::success(
681 "list-commands",
682 serialize_or_exit(&commands, "command list"),
683 );
684 println!("{}", pretty_or_exit(&result, "command result"));
685 return Some(0);
686 }
687
688 let idx = args.iter().position(|a| a == "--show-output-schema")?;
689 let available = crate::output_schemas::list_schema_commands().join(", ");
690
691 let Some(cmd_name) = args.get(idx + 1) else {
692 let result = crate::output::CommandResult::error(
693 "show-output-schema",
694 &format!("Missing command name. Available: {available}"),
695 "MISSING_ARGUMENT",
696 );
697 println!("{}", pretty_or_exit(&result, "command result"));
698 return Some(1);
699 };
700
701 if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
702 let result = crate::output::CommandResult::success(
703 "show-output-schema",
704 serialize_or_exit(&schema, "output schema"),
705 );
706 println!("{}", pretty_or_exit(&result, "command result"));
707 return Some(0);
708 }
709
710 let result = crate::output::CommandResult::error(
711 "show-output-schema",
712 &format!("Unknown command: {cmd_name}. Available: {available}"),
713 "UNKNOWN_COMMAND",
714 );
715 println!("{}", pretty_or_exit(&result, "command result"));
716 Some(1)
717}