1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3pub mod auth_catalog;
15pub mod backfill;
16#[cfg(feature = "catalog")]
17pub mod catalog;
18pub mod chunking;
19pub mod cli;
20pub mod commands;
21pub mod compose;
22pub mod config;
23pub mod conformance;
24pub mod discovery_matrix;
25pub mod dlq_replay;
26pub mod env_config;
27pub mod env_loader;
28pub mod error;
29pub mod executor;
30pub mod expand;
31pub mod init_template;
32pub mod interpolate;
33#[cfg(feature = "lineage")]
34pub mod lineage_glue;
35#[cfg(any(feature = "cli-tui", feature = "cli-progress"))]
38pub mod livemetrics;
39#[cfg(feature = "mcp")]
40pub mod mcp;
41pub mod merge;
42#[cfg(feature = "notify")]
43pub mod notify;
44pub mod obs;
45pub mod params;
46pub mod partition;
47pub mod pipeline_test;
48#[cfg(feature = "cli-progress")]
49pub mod progress;
50pub mod reconcile;
51pub mod registry;
52pub mod registry_index;
53pub mod replication;
54pub mod scaffold;
55#[cfg(feature = "schedule")]
56pub mod schedule;
57pub mod schema_compose;
58pub mod secrets;
59pub mod select;
60#[cfg(feature = "serve")]
61pub mod serve;
62pub mod sla;
63pub mod state;
64#[cfg(feature = "templates")]
65pub mod templates;
66pub mod topology;
67pub mod transforms;
68#[cfg(feature = "cli-tui")]
69pub mod tui;
70
71pub use error::{CliError, CliResult};
72
73use crate::cli::{Cli, Command};
74use crate::registry::PluginRegistry;
75
76pub fn run_main(registry: PluginRegistry) -> std::process::ExitCode {
97 use clap::Parser;
98 use std::process::ExitCode;
99
100 if let Err(err) = registry.install() {
101 commands::report(&err);
102 return ExitCode::from(1);
103 }
104
105 faucet_core::redact::install(Box::new(|s: &str| {
111 secrets::registry::redact(s).into_owned()
112 }));
113
114 clap_complete::env::CompleteEnv::with_factory(<Cli as clap::CommandFactory>::command)
119 .complete();
120
121 let cli = Cli::parse();
122 #[cfg(feature = "serve")]
123 let is_serve = matches!(cli.command, Command::Serve(_));
124 #[cfg(not(feature = "serve"))]
125 let is_serve = false;
126 #[cfg(feature = "cli-tui")]
129 let is_tui = matches!(&cli.command, Command::Run(a) if tui::is_tui_session(a.tui));
130 #[cfg(not(feature = "cli-tui"))]
131 let is_tui = false;
132 #[cfg(feature = "mcp")]
135 let is_mcp = matches!(cli.command, Command::Mcp(_));
136 #[cfg(not(feature = "mcp"))]
137 let is_mcp = false;
138 if !is_serve && !is_tui && !is_mcp {
141 install_tracing(&cli.log_level);
142 }
143 #[cfg(feature = "cli-tui")]
144 if is_tui {
145 tui::install_tui_tracing(&cli.log_level);
146 }
147 #[cfg(feature = "mcp")]
148 if is_mcp {
149 mcp::install_stderr_tracing(&cli.log_level);
150 }
151
152 let runtime = match tokio::runtime::Builder::new_multi_thread()
153 .enable_all()
154 .build()
155 {
156 Ok(rt) => rt,
157 Err(e) => {
158 eprintln!("error: failed to start async runtime: {e}");
159 return ExitCode::from(1);
160 }
161 };
162
163 runtime.block_on(async move {
164 match run_command(cli).await {
165 Ok(()) => ExitCode::SUCCESS,
166 Err(CliError::DoctorFailed { failed }) => ExitCode::from(failed.min(255) as u8),
169 Err(CliError::TestsFailed { failed }) => ExitCode::from(failed.min(255) as u8),
170 Err(CliError::BackfillFailed { failed }) => ExitCode::from(failed.min(255) as u8),
171 Err(err) => {
172 commands::report(&err);
173 ExitCode::from(1)
174 }
175 }
176 })
177}
178
179pub async fn run_command(cli: Cli) -> CliResult<()> {
184 #[cfg(feature = "serve")]
185 let serve_log_level = cli.log_level.clone();
186 match cli.command {
187 Command::Run(args) => commands::run::run(args).await,
188 Command::Backfill(args) => commands::backfill::run(args).await,
189 Command::Replicate(args) => commands::replicate::run(args).await,
190 Command::Discover(args) => commands::discover::run(args).await,
191 Command::Validate(args) => commands::validate::run(args).await,
192 Command::Schema(args) => commands::schema::run(args).await,
193 Command::List(args) => commands::list::run(args).await,
194 Command::Search(args) => commands::search::run(args).await,
195 Command::Conformance(args) => commands::conformance::run(args).await,
196 Command::Install(args) => commands::install::run(args).await,
197 Command::Preview(args) => commands::preview::run(args).await,
198 Command::Plan(args) => commands::plan::run(args).await,
199 #[cfg(feature = "cli-dev")]
200 Command::Dev(args) => commands::dev::run(args).await,
201 Command::Init(args) => commands::init::run(args).await,
202 Command::New(args) => commands::new::run(args).await,
203 Command::Doctor(args) => commands::doctor::run(args).await,
204 Command::Test(args) => commands::test::run(args).await,
205 Command::Dlq(args) => commands::dlq::run(args).await,
206 #[cfg(feature = "contract")]
207 Command::Contract(args) => commands::contract::run(args).await,
208 #[cfg(feature = "masking")]
209 Command::Masking(args) => commands::masking::run(args).await,
210 #[cfg(feature = "schedule")]
211 Command::Schedule(args) => commands::schedule::run(args).await,
212 #[cfg(feature = "serve")]
213 Command::Serve(args) => commands::serve::run(args, serve_log_level).await,
214 #[cfg(feature = "mcp")]
215 Command::Mcp(args) => commands::mcp::run(args).await,
216 #[cfg(feature = "notify")]
217 Command::Notify(args) => commands::notify::run(args).await,
218 #[cfg(feature = "catalog")]
219 Command::Catalog(args) => commands::catalog::run(args).await,
220 #[cfg(feature = "templates")]
221 Command::Template(args) => commands::template::run(args).await,
222 Command::Completions(args) => commands::completions::run(args.shell),
223 Command::Migrate(args) => commands::migrate::run(args).await,
224 Command::Fmt(args) => commands::fmt::run(args).await,
225 Command::Explain(args) => commands::explain::run(args).await,
226 #[cfg(feature = "catalog")]
227 Command::History(args) => commands::history::run(args).await,
228 }
229}
230
231#[cfg(feature = "observability")]
232fn install_tracing(level: &str) {
233 use crate::secrets::registry::RedactingMakeWriter;
234 use tracing_subscriber::EnvFilter;
235 let filter = EnvFilter::try_new(level).unwrap_or_else(|_| EnvFilter::new("info"));
236 let _ = tracing_subscriber::fmt()
237 .with_env_filter(filter)
238 .with_writer(RedactingMakeWriter)
239 .try_init();
240}
241
242#[cfg(not(feature = "observability"))]
245fn install_tracing(_level: &str) {}
246
247pub async fn run_from_yaml_str(yaml: &str) -> CliResult<executor::RunSummary> {
254 let mut value: serde_json::Value =
258 serde_yaml::from_str(yaml).map_err(|e| CliError::ParseConfig {
259 path: std::path::PathBuf::from("<yaml-string>"),
260 message: e.to_string(),
261 })?;
262 interpolate::interpolate_value(&mut value)?;
263 params::bind_document(&mut value, &Default::default(), params::BindMode::Strict)?;
267 let interpolated = serde_yaml::to_string(&value).map_err(|e| CliError::ParseConfig {
268 path: std::path::PathBuf::from("<yaml-string>"),
269 message: e.to_string(),
270 })?;
271 let mut cfg: config::PipelineConfig =
272 serde_yaml::from_str(&interpolated).map_err(|e| CliError::ParseConfig {
273 path: std::path::PathBuf::from("<yaml-string>"),
274 message: e.to_string(),
275 })?;
276 if cfg.version != 1 {
277 return Err(CliError::ParseConfig {
278 path: std::path::PathBuf::from("<yaml-string>"),
279 message: format!(
280 "unsupported pipeline version {}, only version 1 is recognised",
281 cfg.version
282 ),
283 });
284 }
285 crate::secrets::resolve_secrets(&mut cfg).await?;
286 let pipeline_name = cfg.name.clone().unwrap_or_else(|| "unnamed".to_string());
287 let auth = auth_catalog::build_auth_catalog(cfg.auth.as_ref())?;
288 let resilience = match &cfg.resilience {
289 Some(spec) => Some(spec.to_policy()?),
290 None => None,
291 };
292 #[cfg(feature = "catalog")]
293 let catalog = match cfg.catalog.as_ref() {
294 Some(spec) => Some(catalog::connect_from_spec(spec).await?),
295 None => None,
296 };
297 let nodes = expand::expand(&cfg)?;
298 executor::run_expanded(
299 nodes,
300 executor::ExecuteOptions {
301 pipeline_name,
302 run_id: None,
303 execution: cfg.execution.clone(),
304 dry_run: false,
305 limit: None,
306 state_path_override: None,
307 shard: None,
308 auth,
309 clock: chrono::Utc::now().fixed_offset(),
310 cancel: None,
311 resilience,
312 sla: cfg.sla.clone(),
313 reconcile: cfg.reconcile.clone(),
314 #[cfg(feature = "lineage")]
315 lineage: None,
316 #[cfg(feature = "lineage")]
317 lineage_cfg: None,
318 #[cfg(feature = "notify")]
319 notifier: None,
320 #[cfg(feature = "catalog")]
321 catalog,
322 },
323 )
324 .await
325}