1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3pub mod auth_catalog;
15pub mod backfill;
16#[cfg(feature = "catalog")]
17pub mod catalog;
18pub mod cli;
19pub mod commands;
20pub mod compose;
21pub mod config;
22pub mod conformance;
23pub mod dlq_replay;
24pub mod env_config;
25pub mod env_loader;
26pub mod error;
27pub mod executor;
28pub mod expand;
29pub mod init_template;
30pub mod interpolate;
31#[cfg(feature = "lineage")]
32pub mod lineage_glue;
33#[cfg(any(feature = "cli-tui", feature = "cli-progress"))]
36pub mod livemetrics;
37#[cfg(feature = "mcp")]
38pub mod mcp;
39pub mod merge;
40#[cfg(feature = "notify")]
41pub mod notify;
42pub mod obs;
43pub mod pipeline_test;
44#[cfg(feature = "cli-progress")]
45pub mod progress;
46pub mod registry;
47pub mod registry_index;
48pub mod replication;
49pub mod scaffold;
50#[cfg(feature = "schedule")]
51pub mod schedule;
52pub mod schema_compose;
53pub mod secrets;
54pub mod select;
55#[cfg(feature = "serve")]
56pub mod serve;
57pub mod sla;
58pub mod state;
59pub mod topology;
60pub mod transforms;
61#[cfg(feature = "cli-tui")]
62pub mod tui;
63
64pub use error::{CliError, CliResult};
65
66use crate::cli::{Cli, Command};
67use crate::registry::PluginRegistry;
68
69pub fn run_main(registry: PluginRegistry) -> std::process::ExitCode {
90 use clap::Parser;
91 use std::process::ExitCode;
92
93 if let Err(err) = registry.install() {
94 commands::report(&err);
95 return ExitCode::from(1);
96 }
97
98 clap_complete::env::CompleteEnv::with_factory(<Cli as clap::CommandFactory>::command)
103 .complete();
104
105 let cli = Cli::parse();
106 #[cfg(feature = "serve")]
107 let is_serve = matches!(cli.command, Command::Serve(_));
108 #[cfg(not(feature = "serve"))]
109 let is_serve = false;
110 #[cfg(feature = "cli-tui")]
113 let is_tui = matches!(&cli.command, Command::Run(a) if tui::is_tui_session(a.tui));
114 #[cfg(not(feature = "cli-tui"))]
115 let is_tui = false;
116 #[cfg(feature = "mcp")]
119 let is_mcp = matches!(cli.command, Command::Mcp(_));
120 #[cfg(not(feature = "mcp"))]
121 let is_mcp = false;
122 if !is_serve && !is_tui && !is_mcp {
125 install_tracing(&cli.log_level);
126 }
127 #[cfg(feature = "cli-tui")]
128 if is_tui {
129 tui::install_tui_tracing(&cli.log_level);
130 }
131 #[cfg(feature = "mcp")]
132 if is_mcp {
133 mcp::install_stderr_tracing(&cli.log_level);
134 }
135
136 let runtime = match tokio::runtime::Builder::new_multi_thread()
137 .enable_all()
138 .build()
139 {
140 Ok(rt) => rt,
141 Err(e) => {
142 eprintln!("error: failed to start async runtime: {e}");
143 return ExitCode::from(1);
144 }
145 };
146
147 runtime.block_on(async move {
148 match run_command(cli).await {
149 Ok(()) => ExitCode::SUCCESS,
150 Err(CliError::DoctorFailed { failed }) => ExitCode::from(failed.min(255) as u8),
153 Err(CliError::TestsFailed { failed }) => ExitCode::from(failed.min(255) as u8),
154 Err(CliError::BackfillFailed { failed }) => ExitCode::from(failed.min(255) as u8),
155 Err(err) => {
156 commands::report(&err);
157 ExitCode::from(1)
158 }
159 }
160 })
161}
162
163pub async fn run_command(cli: Cli) -> CliResult<()> {
168 #[cfg(feature = "serve")]
169 let serve_log_level = cli.log_level.clone();
170 match cli.command {
171 Command::Run(args) => commands::run::run(args).await,
172 Command::Backfill(args) => commands::backfill::run(args).await,
173 Command::Replicate(args) => commands::replicate::run(args).await,
174 Command::Discover(args) => commands::discover::run(args).await,
175 Command::Validate(args) => commands::validate::run(args).await,
176 Command::Schema(args) => commands::schema::run(args).await,
177 Command::List(args) => commands::list::run(args).await,
178 Command::Search(args) => commands::search::run(args).await,
179 Command::Conformance(args) => commands::conformance::run(args).await,
180 Command::Install(args) => commands::install::run(args).await,
181 Command::Preview(args) => commands::preview::run(args).await,
182 Command::Plan(args) => commands::plan::run(args).await,
183 #[cfg(feature = "cli-dev")]
184 Command::Dev(args) => commands::dev::run(args).await,
185 Command::Init(args) => commands::init::run(args).await,
186 Command::New(args) => commands::new::run(args).await,
187 Command::Doctor(args) => commands::doctor::run(args).await,
188 Command::Test(args) => commands::test::run(args).await,
189 Command::Dlq(args) => commands::dlq::run(args).await,
190 #[cfg(feature = "contract")]
191 Command::Contract(args) => commands::contract::run(args).await,
192 #[cfg(feature = "masking")]
193 Command::Masking(args) => commands::masking::run(args).await,
194 #[cfg(feature = "schedule")]
195 Command::Schedule(args) => commands::schedule::run(args).await,
196 #[cfg(feature = "serve")]
197 Command::Serve(args) => commands::serve::run(args, serve_log_level).await,
198 #[cfg(feature = "mcp")]
199 Command::Mcp(args) => commands::mcp::run(args).await,
200 #[cfg(feature = "notify")]
201 Command::Notify(args) => commands::notify::run(args).await,
202 #[cfg(feature = "catalog")]
203 Command::Catalog(args) => commands::catalog::run(args).await,
204 Command::Completions(args) => commands::completions::run(args.shell),
205 Command::Migrate(args) => commands::migrate::run(args).await,
206 Command::Fmt(args) => commands::fmt::run(args).await,
207 Command::Explain(args) => commands::explain::run(args).await,
208 #[cfg(feature = "catalog")]
209 Command::History(args) => commands::history::run(args).await,
210 }
211}
212
213#[cfg(feature = "observability")]
214fn install_tracing(level: &str) {
215 use crate::secrets::registry::RedactingMakeWriter;
216 use tracing_subscriber::EnvFilter;
217 let filter = EnvFilter::try_new(level).unwrap_or_else(|_| EnvFilter::new("info"));
218 let _ = tracing_subscriber::fmt()
219 .with_env_filter(filter)
220 .with_writer(RedactingMakeWriter)
221 .try_init();
222}
223
224#[cfg(not(feature = "observability"))]
227fn install_tracing(_level: &str) {}
228
229pub async fn run_from_yaml_str(yaml: &str) -> CliResult<executor::RunSummary> {
236 let mut value: serde_json::Value =
240 serde_yaml::from_str(yaml).map_err(|e| CliError::ParseConfig {
241 path: std::path::PathBuf::from("<yaml-string>"),
242 message: e.to_string(),
243 })?;
244 interpolate::interpolate_value(&mut value)?;
245 let interpolated = serde_yaml::to_string(&value).map_err(|e| CliError::ParseConfig {
246 path: std::path::PathBuf::from("<yaml-string>"),
247 message: e.to_string(),
248 })?;
249 let mut cfg: config::PipelineConfig =
250 serde_yaml::from_str(&interpolated).map_err(|e| CliError::ParseConfig {
251 path: std::path::PathBuf::from("<yaml-string>"),
252 message: e.to_string(),
253 })?;
254 if cfg.version != 1 {
255 return Err(CliError::ParseConfig {
256 path: std::path::PathBuf::from("<yaml-string>"),
257 message: format!(
258 "unsupported pipeline version {}, only version 1 is recognised",
259 cfg.version
260 ),
261 });
262 }
263 crate::secrets::resolve_secrets(&mut cfg).await?;
264 let pipeline_name = cfg.name.clone().unwrap_or_else(|| "unnamed".to_string());
265 let auth = auth_catalog::build_auth_catalog(cfg.auth.as_ref())?;
266 let resilience = match &cfg.resilience {
267 Some(spec) => Some(spec.to_policy()?),
268 None => None,
269 };
270 #[cfg(feature = "catalog")]
271 let catalog = match cfg.catalog.as_ref() {
272 Some(spec) => Some(catalog::connect_from_spec(spec).await?),
273 None => None,
274 };
275 let nodes = expand::expand(&cfg)?;
276 executor::run_expanded(
277 nodes,
278 executor::ExecuteOptions {
279 pipeline_name,
280 execution: cfg.execution.clone(),
281 dry_run: false,
282 limit: None,
283 state_path_override: None,
284 shard: None,
285 auth,
286 clock: chrono::Utc::now().fixed_offset(),
287 cancel: None,
288 resilience,
289 sla: cfg.sla.clone(),
290 #[cfg(feature = "lineage")]
291 lineage: None,
292 #[cfg(feature = "lineage")]
293 lineage_cfg: None,
294 #[cfg(feature = "notify")]
295 notifier: None,
296 #[cfg(feature = "catalog")]
297 catalog,
298 },
299 )
300 .await
301}