use std::{env, process, str::FromStr};
use clap::{CommandFactory, Parser};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use crate::cli::{
Cli, Commands, FederationCommands, GenerateClientCommands, IntrospectCommands, MigrateCommands,
PerfCommands, PerfExploreCommands, SchemaCommands, ValidateCommands,
};
#[allow(clippy::cognitive_complexity)] pub async fn run() {
use crate::{commands, output};
if let Some(code) = handle_introspection_flags() {
process::exit(code);
}
let cli = Cli::parse();
init_logging(cli.verbose, cli.debug);
let json_output = cli.json;
let debug_output = cli.debug;
let result = match cli.command {
Commands::Compile {
input,
types,
schema_dir,
type_files,
query_files,
mutation_files,
output,
check,
skip_hash,
database,
emit_ddl,
check_migrations,
allow_drift,
config,
} => {
commands::compile::run(
&input,
types.as_deref(),
schema_dir.as_deref(),
type_files,
query_files,
mutation_files,
&output,
check,
database.as_deref(),
emit_ddl.as_deref(),
check_migrations,
skip_hash,
allow_drift,
config.as_deref(),
)
.await
},
Commands::Extract {
input,
language,
recursive,
output,
} => commands::extract::run(&input, language.as_deref(), recursive, &output),
Commands::Explain { query } => match commands::explain::run(&query) {
Ok(result) => {
println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
Ok(())
},
Err(e) => Err(e),
},
Commands::Cost { query } => match commands::cost::run(&query) {
Ok(result) => {
println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
Ok(())
},
Err(e) => Err(e),
},
Commands::Analyze { schema } => match commands::analyze::run(&schema) {
Ok(result) => {
println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
Ok(())
},
Err(e) => Err(e),
},
Commands::DependencyGraph { schema, format } => {
match commands::dependency_graph::GraphFormat::from_str(&format) {
Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
Ok(result) => {
println!(
"{}",
output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
);
Ok(())
},
Err(e) => Err(e),
},
Err(e) => Err(anyhow::anyhow!(e)),
}
},
Commands::Lint {
schema,
federation,
cost,
cache,
auth,
compilation,
fail_on_critical,
fail_on_warning,
} => {
let opts = commands::lint::LintOptions {
fail_on_critical,
fail_on_warning,
filter: commands::lint::LintCategoryFilter {
federation,
cost,
cache,
auth,
compilation,
},
};
match commands::lint::run(&schema, opts) {
Ok(result) => {
println!(
"{}",
output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
);
enforce_exit_code(&result);
Ok(())
},
Err(e) => Err(e),
}
},
Commands::Federation { command } => match command {
FederationCommands::Graph { schema, format } => {
match commands::federation::graph::GraphFormat::from_str(&format) {
Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
Ok(result) => {
println!(
"{}",
output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
);
Ok(())
},
Err(e) => Err(e),
},
Err(e) => Err(anyhow::anyhow!(e)),
}
},
#[cfg(feature = "federation")]
FederationCommands::Sdl { schema, output } => {
commands::federation::sdl::run(&schema, output.as_deref())
},
FederationCommands::Check {
schema,
against,
json,
} => {
match commands::federation::check::run(
&schema,
against.as_deref(),
json || cli.json,
) {
Ok(result) => {
if !(json || cli.json) {
println!(
"{}",
output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
);
}
enforce_exit_code(&result);
Ok(())
},
Err(e) => Err(e),
}
},
},
Commands::GenerateViews {
schema,
entity,
view,
refresh_strategy,
output,
include_composition_views,
include_monitoring,
validate,
gen_verbose,
} => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
Ok(refresh_strat) => {
let config = commands::generate_views::GenerateViewsConfig {
schema_path: schema,
entity,
view,
refresh_strategy: refresh_strat,
output,
include_composition_views,
include_monitoring,
validate_only: validate,
verbose: cli.verbose || gen_verbose,
};
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
commands::generate_views::run(config, &formatter).await
},
Err(e) => Err(anyhow::anyhow!(e)),
},
Commands::GenerateCaptureTriggers {
schema,
output,
no_function,
} => {
let config = commands::generate_capture_triggers::GenerateCaptureTriggersConfig {
schema_path: schema,
output,
include_function: !no_function,
};
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
commands::generate_capture_triggers::run(config, &formatter)
},
Commands::Validate {
command,
input,
check_cycles,
check_unused,
strict,
types,
against_db,
} => match command {
Some(ValidateCommands::Facts { schema, database }) => {
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
.await
},
None => match (against_db, input) {
(Some(db_url), Some(input)) => {
commands::validate::run_against_db(&input, &db_url, cli.json).await
},
(Some(_), None) => Err(anyhow::anyhow!(
"INPUT (schema.compiled.json) is required with --against-db"
)),
(None, Some(input)) => {
let opts = commands::validate::ValidateOptions {
check_cycles,
check_unused,
strict,
filter_types: types,
};
match commands::validate::run_with_options(&input, opts) {
Ok(result) => {
println!(
"{}",
output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
);
enforce_exit_code(&result);
Ok(())
},
Err(e) => Err(e),
}
},
(None, None) => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
},
},
Commands::Introspect { command } => match command {
IntrospectCommands::Facts { database, format } => {
match commands::introspect_facts::OutputFormat::parse(&format) {
Ok(fmt) => {
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
commands::introspect_facts::run(&database, fmt, &formatter).await
},
Err(e) => Err(anyhow::anyhow!(e)),
}
},
},
Commands::Generate {
input,
language,
output,
} => match commands::init::Language::from_str(&language) {
Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
Err(e) => Err(anyhow::anyhow!(e)),
},
Commands::GenerateClient { language } => match language {
GenerateClientCommands::Typescript { schema, out, force } => {
commands::generate_client::run(
commands::generate_client::ClientLanguage::TypeScript,
schema.as_deref(),
&out,
force,
)
},
GenerateClientCommands::Python { schema, out, force } => {
commands::generate_client::run(
commands::generate_client::ClientLanguage::Python,
schema.as_deref(),
&out,
force,
)
},
GenerateClientCommands::Go { schema, out, force } => commands::generate_client::run(
commands::generate_client::ClientLanguage::Go,
schema.as_deref(),
&out,
force,
),
GenerateClientCommands::Rust { schema, out, force } => commands::generate_client::run(
commands::generate_client::ClientLanguage::Rust,
schema.as_deref(),
&out,
force,
),
},
Commands::Init {
project_name,
language,
database,
size,
no_git,
} => {
match (
commands::init::Language::from_str(&language),
commands::init::Database::from_str(&database),
commands::init::ProjectSize::from_str(&size),
) {
(Ok(lang), Ok(db), Ok(sz)) => {
let config = commands::init::InitConfig {
project_name,
language: lang,
database: db,
size: sz,
no_git,
};
commands::init::run(&config)
},
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
}
},
Commands::Migrate { command } => {
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
match command {
MigrateCommands::Up { database, dir } => {
match migrate_database(database.as_deref(), &formatter).await {
Ok(url) => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Up {
database_url: url,
dir: mig_dir,
};
commands::migrate::run(&action, &formatter)
},
Err(e) => Err(e),
}
},
MigrateCommands::Down {
database,
dir,
steps,
} => match migrate_database(database.as_deref(), &formatter).await {
Ok(url) => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Down {
database_url: url,
dir: mig_dir,
steps,
};
commands::migrate::run(&action, &formatter)
},
Err(e) => Err(e),
},
MigrateCommands::Status { database, dir } => {
match migrate_database(database.as_deref(), &formatter).await {
Ok(url) => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Status {
database_url: url,
dir: mig_dir,
};
commands::migrate::run(&action, &formatter)
},
Err(e) => Err(e),
}
},
MigrateCommands::Create { name, dir } => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
commands::migrate::run(&action, &formatter)
},
MigrateCommands::Generate { name, dir } => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Generate { name, dir: mig_dir };
commands::migrate::run(&action, &formatter)
},
MigrateCommands::Validate { dir } => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Validate { dir: mig_dir };
commands::migrate::run(&action, &formatter)
},
MigrateCommands::Preflight { dir } => {
let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
let action = commands::migrate::MigrateAction::Preflight { dir: mig_dir };
commands::migrate::run(&action, &formatter)
},
}
},
Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
Err(e) => Err(anyhow::anyhow!(e)),
},
#[cfg(feature = "run-server")]
Commands::Capabilities => commands::capabilities::run(cli.json, cli.quiet),
#[cfg(feature = "run-server")]
Commands::Auth {
command: crate::cli::AuthCommands::RotateSigningKey { input, config },
} => {
Box::pin(commands::auth::rotate_signing_key(input.as_deref(), config.as_deref())).await
},
#[cfg(feature = "run-server")]
Commands::Run {
input,
database,
port,
bind,
watch,
introspection,
admin_database_url,
} => {
Box::pin(commands::run::run(
input.as_deref(),
database,
port,
bind,
watch,
introspection,
admin_database_url,
))
.await
},
Commands::Watch {
input,
output,
reload_url,
admin_token,
database,
} => {
commands::watch::run(
&input,
&output,
reload_url.as_deref(),
admin_token.as_deref(),
database.as_deref(),
)
.await
},
Commands::ValidateDocuments {
manifest,
max_cost,
schema,
} => {
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
match commands::validate_documents::run(
&manifest,
max_cost,
schema.as_deref(),
&formatter,
) {
Ok(true) => Ok(()),
Ok(false) => {
process::exit(2);
},
Err(e) => Err(e),
}
},
Commands::Setup { database, dry_run } => {
let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
commands::setup::run(database.as_deref(), dry_run, &formatter).await
},
Commands::Schema { command } => match command {
SchemaCommands::Metadata { server, token } => {
commands::schema::metadata::run(&server, token.as_deref()).await
},
},
Commands::Query {
query,
schema,
database,
variables,
dry_run,
} => {
Box::pin(commands::query::run(&query, &schema, database, variables, dry_run)).await
},
Commands::Doctor {
config,
schema,
db_url,
against_db,
runtime,
schemas,
json: json_flag,
} => {
let all_passed = commands::doctor::run_with_db_checks(
&config,
&schema,
db_url.as_deref(),
against_db.as_deref(),
runtime,
&schemas,
json_flag,
)
.await;
if all_passed {
Ok(())
} else {
process::exit(1);
}
},
Commands::Perf { command } => match command {
PerfCommands::RegressionScan {
database,
recent_days,
baseline_days,
min_samples,
threshold_pct,
min_delta_ms,
object_type,
fail_on_regression,
} => {
let args = commands::perf::RegressionScanArgs {
database,
recent_days,
baseline_days,
min_samples,
threshold_pct,
min_delta_ms,
object_type,
fail_on_regression,
json: cli.json,
quiet: cli.quiet,
};
match commands::perf::run_regression_scan(args).await {
Ok(true) => Ok(()),
Ok(false) => process::exit(1),
Err(e) => Err(e),
}
},
PerfCommands::Explore { command } => match command {
PerfExploreCommands::Slowest {
database,
days,
object_type,
limit,
} => {
commands::perf::run_explore_slowest(
database,
days,
object_type,
limit,
cli.json,
cli.quiet,
)
.await
},
PerfExploreCommands::NullRate {
database,
days,
object_type,
} => {
commands::perf::run_explore_null_rate(
database,
days,
object_type,
cli.json,
cli.quiet,
)
.await
},
PerfExploreCommands::Summary {
database,
days,
object_type,
} => {
commands::perf::run_explore_summary(
database,
days,
object_type,
cli.json,
cli.quiet,
)
.await
},
},
},
Commands::Sources {
schema,
db_url,
json,
} => {
commands::sources::run(commands::sources::SourcesArgs {
schema,
database: db_url,
json,
})
.await
},
#[cfg(feature = "functions-invoke")]
Commands::Functions(command) => match command {
crate::cli::FunctionsCommands::Invoke {
name,
payload,
schema,
mock_http,
mock_query,
idempotency_token,
explain,
} => match commands::functions::invoke(
&name,
&payload,
&schema,
mock_http.as_deref(),
mock_query.as_deref(),
idempotency_token,
explain,
cli.json,
)
.await
{
Ok(code) => process::exit(code),
Err(error) => Err(error),
},
},
};
if let Err(e) = result {
let debug_info = if debug_output {
Some(format!("{e:?}"))
} else {
None
};
let causes = error_causes(&e);
eprintln!(
"{}",
format_cli_error(&e.to_string(), &causes, debug_info.as_deref(), json_output, 1)
);
process::exit(1);
}
}
fn enforce_exit_code(result: &crate::output::CommandResult) {
match result.status.as_str() {
"success" => {},
"validation-failed" => process::exit(2),
_ => process::exit(1),
}
}
pub(crate) fn error_causes(err: &anyhow::Error) -> Vec<String> {
err.chain().skip(1).map(ToString::to_string).collect()
}
pub(crate) fn format_cli_error(
message: &str,
causes: &[String],
debug_info: Option<&str>,
json: bool,
code: i32,
) -> String {
if json {
serde_json::json!({
"error": {
"message": message,
"causes": causes,
"code": code
}
})
.to_string()
} else {
let mut out = format!("Error: {message}");
for cause in causes {
out.push_str(&format!("\n caused by: {cause}"));
}
if let Some(debug) = debug_info {
out.push_str("\n\nDebug info:\n");
out.push_str(debug);
}
out
}
}
fn init_logging(verbose: bool, debug: bool) {
let filter = if debug {
"fraiseql=debug,fraiseql_core=debug"
} else if verbose {
"fraiseql=info,fraiseql_core=info"
} else {
"fraiseql=warn,fraiseql_core=warn"
};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
)
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.init();
}
fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
serde_json::to_value(value).unwrap_or_else(|e| {
eprintln!("fraiseql: failed to serialize {context}: {e}");
std::process::exit(2);
})
}
fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
serde_json::to_string_pretty(value).unwrap_or_else(|e| {
eprintln!("fraiseql: failed to format {context}: {e}");
std::process::exit(2);
})
}
fn handle_introspection_flags() -> Option<i32> {
let args: Vec<String> = env::args().collect();
if args.iter().any(|a| a == "--help-json") {
let cmd = Cli::command();
let version = env!("CARGO_PKG_VERSION");
let help = crate::introspection::extract_cli_help(&cmd, version);
let result =
crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
println!("{}", pretty_or_exit(&result, "command result"));
return Some(0);
}
if args.iter().any(|a| a == "--list-commands") {
let cmd = Cli::command();
let commands = crate::introspection::list_commands(&cmd);
let result = crate::output::CommandResult::success(
"list-commands",
serialize_or_exit(&commands, "command list"),
);
println!("{}", pretty_or_exit(&result, "command result"));
return Some(0);
}
let idx = args.iter().position(|a| a == "--show-output-schema")?;
let available = crate::output_schemas::list_schema_commands().join(", ");
let Some(cmd_name) = args.get(idx + 1) else {
let result = crate::output::CommandResult::error(
"show-output-schema",
&format!("Missing command name. Available: {available}"),
"MISSING_ARGUMENT",
);
println!("{}", pretty_or_exit(&result, "command result"));
return Some(1);
};
if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
let result = crate::output::CommandResult::success(
"show-output-schema",
serialize_or_exit(&schema, "output schema"),
);
println!("{}", pretty_or_exit(&result, "command result"));
return Some(0);
}
let result = crate::output::CommandResult::error(
"show-output-schema",
&format!("Unknown command: {cmd_name}. Available: {available}"),
"UNKNOWN_COMMAND",
);
println!("{}", pretty_or_exit(&result, "command result"));
Some(1)
}
async fn migrate_database(
explicit: Option<&str>,
formatter: &crate::output::OutputFormatter,
) -> anyhow::Result<String> {
crate::commands::migrate::require_confiture(formatter)?;
let database = crate::commands::migrate::resolve_database(explicit)?;
crate::commands::migrate::refuse_unsupported_server(&database).await?;
Ok(database.url)
}