#[cfg(test)]
mod cli_reference;
mod exit;
mod extensions;
mod history;
mod init;
mod ls;
mod output;
mod plugins;
mod schedule;
mod signals;
mod system;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand};
use dre_core::codes::Code;
use dre_core::project::{self, LoadOptions};
use dre_core::settings;
#[derive(Parser)]
#[command(name = "dre", version = dre_core::version(), about = "DRE, the Declarative Reporting Engine: SQL in, formatted files out")]
struct Cli {
#[command(subcommand)]
command: Command,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(long, global = true, value_enum)]
log_level: Option<output::Verbosity>,
#[arg(long, global = true, value_enum, default_value = "text")]
log_format: output::LogFormat,
#[arg(long, global = true, value_enum, default_value = "auto")]
color: output::ColorChoice,
}
impl Cli {
fn printer(&self) -> output::Printer {
let v = self.log_level.unwrap_or(if self.verbose {
output::Verbosity::Debug
} else if self.quiet {
output::Verbosity::Quiet
} else {
output::Verbosity::Info
});
output::Printer::new(v, self.log_format, self.color)
}
}
#[derive(Subcommand)]
enum Command {
Validate(ValidateArgs),
Run(Box<RunArgs>),
Compile(CompileArgs),
Query(QueryArgs),
Catalog(CatalogArgs),
Mcp(extensions::McpArgs),
#[command(subcommand)]
Extension(extensions::ExtensionCommand),
Clean(CleanArgs),
Ls(ls::LsArgs),
#[command(subcommand)]
Schedule(schedule::ScheduleCommand),
Init(InitArgs),
New(NewArgs),
Deps(DepsArgs),
#[command(subcommand)]
Plugin(PluginCommand),
#[command(subcommand)]
System(system::SystemCommand),
Explain(ExplainArgs),
History(history::HistoryArgs),
Unlock(history::UnlockArgs),
Release(ReleaseArgs),
}
#[derive(Args)]
struct ReleaseArgs {
report: String,
#[arg(long = "run", value_name = "RUN_ID")]
run: String,
#[arg(long, default_value = "default")]
binding: String,
#[arg(long, value_name = "KEYS", value_delimiter = ',')]
only_keys: Option<Vec<String>>,
#[arg(long, value_name = "KEYS", value_delimiter = ',')]
exclude_keys: Option<Vec<String>>,
#[arg(long, value_name = "NAME")]
by: Option<String>,
#[arg(long, value_name = "N/UNIT", value_parser = parse_rate)]
rate: Option<dre_core::rate::Rate>,
#[command(flatten)]
project: ProjectArgs,
}
#[derive(Args)]
struct ExplainArgs {
code: String,
}
#[derive(Args)]
struct QueryArgs {
#[command(flatten)]
project: ProjectArgs,
#[arg(long)]
profile: String,
#[arg(long)]
sql_file: PathBuf,
#[command(flatten)]
output: OutputFile,
#[arg(long, default_value_t = 1000)]
max_rows: u64,
}
#[derive(Args)]
struct CatalogArgs {
#[command(flatten)]
project: ProjectArgs,
#[arg(long)]
profile: String,
#[arg(long)]
tables_file: PathBuf,
#[command(flatten)]
output: OutputFile,
}
#[derive(Args)]
struct OutputFile {
#[arg(long)]
output_file: PathBuf,
}
#[derive(Args)]
struct RunArgs {
selector: Vec<String>,
#[arg(short = 's', long = "select", value_name = "SELECTOR", num_args = 1.., action = clap::ArgAction::Append, conflicts_with = "selector")]
select: Vec<String>,
#[command(flatten)]
project: ProjectArgs,
#[arg(long)]
set: Option<String>,
#[arg(long, value_name = "NAME")]
schedule: Option<String>,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
output_name: Option<String>,
#[arg(long)]
output_path: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_deliver: bool,
#[arg(long, value_name = "DIR", conflicts_with = "dry_run")]
output_json: Option<PathBuf>,
#[arg(long, value_name = "DIR", conflicts_with_all = ["dry_run", "output_json", "preview"])]
from_results: Option<PathBuf>,
#[arg(long, value_name = "ROWS", num_args = 0..=1, default_missing_value = "100")]
preview: Option<u64>,
#[arg(long)]
accept_schema_change: bool,
#[arg(long, value_name = "DURATION")]
timeout: Option<String>,
#[arg(long, value_name = "N")]
keep_runs: Option<String>,
#[arg(long, value_name = "N")]
threads: Option<String>,
#[arg(long, value_name = "N")]
row_concurrency: Option<String>,
#[arg(long, conflicts_with_all = ["dry_run", "preview", "no_deliver"])]
hold: bool,
#[arg(long, value_name = "RUN_ID", conflicts_with_all = ["dry_run", "from_results", "output_json"])]
retry_failed: Option<String>,
#[arg(long, value_name = "KEYS", value_delimiter = ',')]
only_keys: Option<Vec<String>>,
#[arg(long, value_name = "N")]
limit_rows: Option<usize>,
#[arg(long, requires = "preview")]
unmask: bool,
#[arg(long, value_name = "N/UNIT", value_parser = parse_rate)]
rate: Option<dre_core::rate::Rate>,
}
#[derive(Args)]
struct CleanArgs {
#[arg(long, default_value = ".")]
project_dir: PathBuf,
#[arg(long)]
prune: bool,
#[arg(long, value_name = "N", requires = "prune")]
keep_runs: Option<String>,
#[arg(long, value_name = "PATH")]
target_path: Option<String>,
}
#[derive(Subcommand)]
enum PluginCommand {
List,
Install(PluginArgs),
Update(PluginArgs),
Remove(PluginArgs),
}
#[derive(Args)]
struct PluginArgs {
plugin: String,
#[arg(long, default_value = ".")]
project_dir: PathBuf,
}
#[derive(Args)]
struct InitArgs {
#[arg(long)]
profiles_dir: Option<PathBuf>,
}
#[derive(Args)]
struct NewArgs {
dir: PathBuf,
#[arg(long, default_value = "warehouse")]
profile: String,
#[arg(long = "type", alias = "source", default_value = "duckdb")]
plugin: String,
}
#[derive(Args)]
struct DepsArgs {
#[arg(long)]
accept_proprietary: bool,
#[arg(long, default_value = ".")]
project_dir: PathBuf,
#[arg(long)]
profiles_dir: Option<PathBuf>,
}
#[derive(Args)]
struct ProjectArgs {
#[arg(long, default_value = ".")]
project_dir: PathBuf,
#[arg(long)]
profiles_dir: Option<PathBuf>,
#[arg(long)]
no_auto_install: bool,
#[arg(long)]
target: Option<String>,
#[arg(long, value_name = "PATH")]
target_path: Option<String>,
#[arg(long = "var", value_name = "NAME=VALUE", value_parser = parse_var)]
vars: Vec<(String, String)>,
#[arg(long)]
timezone: Option<String>,
}
impl ProjectArgs {
fn timezone(&self) -> Option<String> {
settings::flag_or_env(self.timezone.as_deref(), "--timezone", settings::TIMEZONE).map(|(v, _)| v)
}
fn load_options(&self) -> LoadOptions {
LoadOptions {
profiles_dir: self.profiles_dir.clone(),
target: self.target.clone(),
vars: self.vars.iter().cloned().collect(),
target_path: self.target_path.clone(),
date: run_date(),
scheduled_at: run_at().ok().flatten(),
timezone: self.timezone(),
settings: settings::run_settings(self.timezone.as_deref()),
}
}
}
pub(crate) fn parse_var(s: &str) -> Result<(String, String), String> {
match s.split_once('=') {
Some((k, v)) if !k.trim().is_empty() => Ok((k.trim().to_string(), v.to_string())),
_ => Err(format!("expected NAME=VALUE, got `{s}`")),
}
}
#[derive(Args)]
struct ValidateArgs {
selector: Vec<String>,
#[arg(short = 's', long = "select", value_name = "SELECTOR", num_args = 1.., action = clap::ArgAction::Append, conflicts_with = "selector")]
select: Vec<String>,
#[command(flatten)]
project: ProjectArgs,
#[arg(long)]
json: bool,
#[arg(long)]
live: bool,
#[arg(long)]
set: Option<String>,
#[arg(long)]
strict: bool,
#[arg(long)]
all_targets: bool,
}
#[derive(Args)]
struct CompileArgs {
selector: Vec<String>,
#[arg(short = 's', long = "select", value_name = "SELECTOR", num_args = 1.., action = clap::ArgAction::Append, conflicts_with = "selector")]
select: Vec<String>,
#[command(flatten)]
project: ProjectArgs,
#[arg(long)]
set: Option<String>,
}
fn main() -> ExitCode {
dre_protocol::host::set_core_version(dre_core::version());
let cli = Cli::parse();
let printer = cli.printer();
let project_args = match &cli.command {
Command::Validate(a) => Some(&a.project),
Command::Run(a) => Some(&a.project),
Command::Compile(a) => Some(&a.project),
_ => None,
};
if let Some(t) = project_args.and_then(ProjectArgs::timezone)
&& let Err(e) = dre_core::dates::parse_tz(&t)
{
let from = if project_args.is_some_and(|p| p.timezone.is_some()) {
"--timezone"
} else {
"DRE_TIMEZONE"
};
printer.error(&format!("{from}: {e}"));
return exit::not_started();
}
if project_args.is_some()
&& let Err(e) = run_at()
{
printer.error(&e);
return exit::not_started();
}
match cli.command {
Command::Explain(a) => explain(&a.code),
Command::History(a) => history::history(a),
Command::Unlock(a) => history::unlock(a),
Command::Release(a) => release(a, printer),
Command::Validate(a) => validate(a, &printer),
Command::Run(a) => run(*a, printer),
Command::Compile(a) => compile(a, printer),
Command::Query(a) => query(a, printer),
Command::Catalog(a) => catalog(a, printer),
Command::Mcp(a) => extensions::handoff("mcp", a, &printer),
Command::Extension(a) => extensions::manage(a, &printer),
Command::Clean(a) => clean(a),
Command::Ls(a) => ls::ls(a),
Command::Schedule(schedule::ScheduleCommand::Ls(a)) => schedule::ls(a),
Command::System(system::SystemCommand::Update(a)) => system::update(a, &printer),
Command::Deps(a) => deps(a, &printer),
Command::Init(a) => init::init(a.profiles_dir, &printer),
Command::New(a) => init::new(a.dir, a.profile, a.plugin, &printer),
Command::Plugin(PluginCommand::List) => plugin_list(),
Command::Plugin(PluginCommand::Install(a)) => {
plugins::install(a.plugin, a.project_dir, false, &printer)
}
Command::Plugin(PluginCommand::Update(a)) => {
plugins::install(a.plugin, a.project_dir, true, &printer)
}
Command::Plugin(PluginCommand::Remove(a)) => plugins::remove(a.plugin, a.project_dir, &printer),
}
}
fn explain(code: &str) -> ExitCode {
let code = code
.trim()
.trim_start_matches("error[")
.trim_start_matches("warning[")
.trim_end_matches(']');
if let Some(c) = dre_core::codes::Code::parse(code) {
println!(
"{code} ({})\n\n{}\n\n{}\n\n{}#{code}",
c.kind(),
c.summary(),
c.explanation(),
dre_core::codes::REFERENCE_URL
);
return exit::ok();
}
if let Some((plugin, _)) = code.split_once('/') {
println!(
"`{code}` is a code of the `{plugin}` plugin; see its page in the plugins reference: https://getdre.com/docs/plugins/"
);
return exit::ok();
}
let near: Vec<&str> = dre_core::codes::Code::ALL
.iter()
.map(|c| c.as_str())
.filter(|s| s.contains(code) || code.contains(s))
.collect();
let hint = if near.is_empty() {
format!("; every code is listed at {}", dre_core::codes::REFERENCE_URL)
} else {
format!(
"; did you mean {}?",
near.iter()
.map(|s| format!("`{s}`"))
.collect::<Vec<_>>()
.join(", ")
)
};
eprintln!("error: no code `{code}`{hint}");
exit::not_started()
}
fn validate(a: ValidateArgs, printer: &output::Printer) -> ExitCode {
if !a.project.no_auto_install {
plugins::sync_packages(&a.project.project_dir, printer);
}
let selector = selection(&a.select, &a.selector);
let (project, mut diags) = project::load(&a.project.project_dir, &a.project.load_options());
for d in project.iter().flat_map(|p| p.parse_errors.values().flatten()) {
diags.push(d.clone());
}
let report_errors = project
.as_ref()
.map(|p| dre_core::manifest::report_errors(p, &diags))
.unwrap_or_default();
if let Some(p) = &project {
dre_core::secrets::set_enabled(p.mask_secrets);
}
if let Err(e) = write_manifest(project.as_ref(), &report_errors, &a.project) {
diags.error(Code::TargetPathUnwritable, None, None, e);
}
if let Some(p) = &project {
let offline = a.project.no_auto_install || diags.has_errors();
plugins::check_for_validate(p, !a.project.no_auto_install, &mut diags, printer);
check_plugin_uses(p, &a.project, &mut diags);
dre_core::options::check(p, offline, &mut diags);
}
let (plans, targets) = match &project {
Some(p) if !diags.has_errors() => compile_for_validate(p, &selector, &a.set, &a.project, &mut diags),
_ => (Vec::new(), None),
};
if let Some(w) = targets.as_ref().and_then(dre_core::run::RunTargets::mismatch) {
diags.warning(Code::TargetMismatch, None, None, w);
}
if let Some(p) = &project {
let entries = if a.all_targets {
dre_core::options::all_entries(p)
} else {
targets
.iter()
.flat_map(|t| &t.profiles)
.filter(|u| u.deliver)
.map(|u| {
let role = if u.role == "connection" {
dre_core::profiles::Role::Connection
} else {
dre_core::profiles::Role::Destination
};
(role, u.profile.clone(), u.target.clone())
})
.collect()
};
if a.all_targets || !diags.has_errors() {
dre_core::options::check_connections(p, &entries, &mut diags);
}
}
let ok = !diags.has_errors() && !(a.strict && diags.warning_count() > 0);
if a.json {
let out = serde_json::json!({
"ok": ok,
"profiles": project.as_ref().map(|p| serde_json::json!({
"path": p.profiles.path,
"exists": p.profiles.exists(),
"found_by": p.profiles.found_by,
})),
"target": project.as_ref().map(|p| serde_json::json!({
"name": p.target_name,
"from": p.target_from.to_string(),
"profiles": targets.as_ref().map(|t| t.profiles.clone()).unwrap_or_default(),
})),
"settings": project.as_ref().map(|p| &p.settings),
"errors": diags.error_count(),
"warnings": diags.warning_count(),
"diagnostics": diags.sorted(),
"compiled": plans,
"project": project.as_ref().map(|p| dre_core::manifest::build(p, &report_errors)),
});
println!("{}", dre_core::secrets::to_json_pretty(&out).unwrap());
} else {
for d in diags.sorted() {
printer.diag(d);
}
if selector.is_some() {
for p in &plans {
printer.plan(p);
}
} else if !plans.is_empty()
&& let Some(p) = &project
{
println!(
"Compiled {} Binding{} into {}/",
plans.len(),
plural(plans.len()),
shown(p, &p.target_dir.join("compiled")).display()
);
}
if let Some(p) = &project {
printer.line(output::Tone::Note, "Profiles", &profiles_line(&p.profiles));
let line = targets.as_ref().map_or_else(|| target_line(p), |t| t.line());
printer.line(output::Tone::Note, "Target", &line);
for s in p.settings.lines() {
printer.detail(output::Tone::Note, "Setting", &s);
}
}
let (e, w) = (diags.error_count(), diags.warning_count());
let verdict = if ok { "passed" } else { "failed" };
println!(
"Validation {verdict}: {e} error{}, {w} warning{}",
plural(e),
plural(w)
);
}
if project.is_none() {
return exit::not_started();
}
if !ok {
return exit::failed();
}
if a.live
&& let Some(project) = project
{
return validate_live(&project, selector, a.set, &a.project, printer.clone());
}
exit::ok()
}
#[derive(Default)]
struct Collect {
plans: Vec<dre_core::run::BindingPlan>,
targets: Option<dre_core::run::RunTargets>,
}
impl dre_core::run::Ui for Collect {
fn step(&mut self, _: dre_core::run::Level, _: &str, _: &str, _: Option<std::time::Duration>) {}
fn warn(&mut self, _: &str) {}
fn targets(&mut self, t: &dre_core::run::RunTargets) {
self.targets = Some(t.clone());
}
fn compiled(&mut self, plan: &dre_core::run::BindingPlan) {
self.plans.push(plan.clone());
}
fn choose_set(&mut self, _: &str, _: &[String]) -> Result<Option<String>, String> {
Ok(None)
}
fn plugin_log(&self) -> dre_protocol::host::LogSink {
std::sync::Arc::new(|_, _| {})
}
}
fn compile_for_validate(
project: &dre_core::project::Project,
selector: &Option<String>,
set: &Option<String>,
p: &ProjectArgs,
diags: &mut dre_core::Diagnostics,
) -> (Vec<dre_core::run::BindingPlan>, Option<dre_core::run::RunTargets>) {
let opts = dre_core::run::RunOptions {
selector: selector.clone(),
set: Some(set.clone().unwrap_or_else(|| "all".into())),
target: p.target.clone(),
vars: p.vars.iter().cloned().collect(),
date: run_date(),
scheduled_at: run_at().ok().flatten(),
timezone: p.timezone(),
dry_run: true,
..Default::default()
};
let mut ui = Collect::default();
let summary = dre_core::run::run(project, &opts, &mut ui);
if let Some(e) = summary.error {
let code = if summary.missing_entry {
Code::MissingTargetEntry
} else {
Code::InvalidSelector
};
diags.error(code, None, None, e);
}
for o in summary
.outcomes
.iter()
.filter(|o| o.status == dre_core::run::Status::Error)
{
let set = o.set.as_ref().map(|s| format!(", Set `{s}`")).unwrap_or_default();
let err = o.error.clone().unwrap_or_default();
if err.contains("run_query() failed:") || (err.contains("`columns('") && err.contains("')` failed:"))
{
diags.warning(
Code::CompileNeedsRun,
None,
None,
format!(
"report `{}`{set} queries the database while rendering and can only be checked by `dre run`: {err}",
o.report
),
);
continue;
}
diags.error(
Code::CompileFailed,
None,
None,
format!(
"report `{}`{set} doesn't compile: {}",
o.report,
o.error.clone().unwrap_or_default()
),
);
}
(ui.plans, ui.targets)
}
fn catalog(a: CatalogArgs, printer: output::Printer) -> ExitCode {
json_command(&a.project, a.output, &printer, |project| {
let request =
std::fs::read_to_string(project.root.join(&a.tables_file)).map_err(|e| e.to_string())?;
let request =
serde_json::from_str(&request).map_err(|e| format!("invalid catalogue request: {e}"))?;
dre_core::catalog::build(project, &a.profile, &request)
})
}
fn query(a: QueryArgs, printer: output::Printer) -> ExitCode {
json_command(&a.project, a.output, &printer, |project| {
let sql = std::fs::read_to_string(project.root.join(&a.sql_file)).map_err(|e| e.to_string())?;
dre_core::query::execute(project, &a.profile, &sql, a.max_rows)
})
}
fn json_command(
args: &ProjectArgs,
output: OutputFile,
printer: &output::Printer,
build: impl FnOnce(&dre_core::project::Project) -> Result<serde_json::Value, String>,
) -> ExitCode {
if !args.no_auto_install && !plugins::sync_packages(&args.project_dir, printer) {
return exit::not_started();
}
let Some((project, _)) = load_for_run(args, printer) else {
return exit::not_started();
};
let file = output.output_file;
let result = build(&project).and_then(|value| {
let path = project.root.join(&file);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
dre_core::fs::write_atomic(&path, serde_json::to_vec(&value).unwrap().as_slice())
.map_err(|e| e.to_string())
});
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
printer.error(&error);
exit::not_started()
}
}
}
fn compile(a: CompileArgs, mut printer: output::Printer) -> ExitCode {
if !a.project.no_auto_install && !plugins::sync_packages(&a.project.project_dir, &printer) {
return exit::not_started();
}
let Some((project, manifest_checksum)) = load_for_run(&a.project, &printer) else {
return exit::not_started();
};
if !a.project.no_auto_install && !plugins::ensure(&project, true, false, &printer) {
return exit::not_started();
}
let mut diags = dre_core::Diagnostics::default();
check_plugin_uses(&project, &a.project, &mut diags);
if !report_diags(&diags, &printer) {
return exit::not_started();
}
let opts = dre_core::run::RunOptions {
selector: selection(&a.select, &a.selector),
set: a.set,
target: a.project.target.clone(),
vars: a.project.vars.iter().cloned().collect(),
date: run_date(),
scheduled_at: run_at().ok().flatten(),
timezone: a.project.timezone(),
dry_run: true,
interactive: {
use std::io::IsTerminal;
std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
},
manifest_checksum,
..Default::default()
};
let summary = dre_core::run::run(&project, &opts, &mut printer);
if let Some(e) = &summary.error {
printer.error(e);
return ExitCode::from(summary.exit_code());
}
printer.finish("compile");
ExitCode::from(summary.exit_code())
}
fn validate_live(
project: &dre_core::project::Project,
selector: Option<String>,
set: Option<String>,
p: &ProjectArgs,
mut printer: output::Printer,
) -> ExitCode {
let opts = dre_core::run::RunOptions {
selector,
set,
target: p.target.clone(),
vars: p.vars.iter().cloned().collect(),
date: run_date(),
scheduled_at: run_at().ok().flatten(),
timezone: p.timezone(),
live_check: true,
..Default::default()
};
let summary = dre_core::run::run(project, &opts, &mut printer);
if let Some(e) = &summary.error {
printer.error(e);
return ExitCode::from(summary.exit_code());
}
printer.finish("validate --live");
ExitCode::from(summary.exit_code())
}
fn selection(select: &[String], positional: &[String]) -> Option<String> {
let all = if select.is_empty() { positional } else { select };
(!all.is_empty()).then(|| all.join(" "))
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
fn plugin_list() -> ExitCode {
let here = std::path::Path::new(".");
let in_project = here.join(dre_core::project::PROJECT_FILE).is_file();
let dir = dre_core::plugins::plugins_dir(in_project.then_some(here));
let found = dre_core::plugins::discover(&dir);
if found.is_empty() {
println!("No plugin packages installed in {}", dir.display());
return exit::ok();
}
let quiet: dre_protocol::host::LogSink = std::sync::Arc::new(|_, _| {});
let mut rows = vec![[
"PACKAGE".to_string(),
"PROVIDES".into(),
"VERSION".into(),
"PROTOCOL".into(),
"PATH".into(),
]];
for p in found {
let first = p.provides.first();
let (version, protocol) =
match dre_protocol::host::PluginProcess::start_for(&p.path, first, quiet.clone(), None) {
Ok(proc_) => {
let info = proc_.info().clone();
let _ = proc_.close();
(info.version, format!("v{}", info.protocol_version))
}
Err(e) => (
p.version.map(|v| v.to_string()).unwrap_or_else(|| "?".into()),
format!("error: {e}"),
),
};
let provides: Vec<String> = p.provides.iter().map(|i| i.to_string()).collect();
rows.push([
p.name,
provides.join(", "),
version,
protocol,
p.path.display().to_string(),
]);
}
let widths: Vec<usize> = (0..5)
.map(|i| rows.iter().map(|r| r[i].len()).max().unwrap_or(0))
.collect();
for r in rows {
let line: Vec<String> = r
.iter()
.enumerate()
.map(|(i, c)| format!("{c:<w$}", w = widths[i]))
.collect();
println!("{}", line.join(" ").trim_end());
}
exit::ok()
}
fn profiles_line(p: &dre_core::profiles::Profiles) -> String {
let missing = if p.exists() { "" } else { ", not found" };
format!("{} (from {}{missing})", p.path.display(), p.found_by)
}
fn target_line(p: &dre_core::project::Project) -> String {
format!("{} ({})", p.target_name, p.target_from)
}
fn load_for_run(
args: &ProjectArgs,
printer: &output::Printer,
) -> Option<(dre_core::project::Project, Option<String>)> {
let (project, diags) = project::load(&args.project_dir, &args.load_options());
if let Some(p) = &project {
dre_core::secrets::set_enabled(p.mask_secrets);
}
let errors = project
.as_ref()
.map(|p| dre_core::manifest::report_errors(p, &diags))
.unwrap_or_default();
let checksum = match write_manifest(project.as_ref(), &errors, args) {
Ok(c) => c,
Err(e) => {
printer.error(&e);
return None;
}
};
for d in diags.sorted() {
if d.severity == dre_core::Severity::Warning && d.code == "unmanaged-report" {
continue;
}
printer.diag(d);
}
if diags.has_errors() {
printer.error(&format!(
"the project has {} error(s); fix them before running (see `dre validate`)",
diags.error_count()
));
return None;
}
project.map(|p| (p, checksum))
}
fn write_manifest(
project: Option<&dre_core::project::Project>,
errors: &dre_core::manifest::ReportErrors,
args: &ProjectArgs,
) -> Result<Option<String>, String> {
let Some(p) = project else {
let root = &args.project_dir;
let from_file = dre_core::target::project_value(root);
if let Ok(t) = dre_core::target::resolve(root, args.target_path.as_deref(), from_file.as_deref()) {
dre_core::manifest::remove(&t.dir);
}
return Ok(None);
};
dre_core::manifest::write(p, errors)
.map(Some)
.map_err(|e| format!("{e} (target path from {})", p.target_source))
}
fn shown(project: &dre_core::project::Project, p: &std::path::Path) -> PathBuf {
dre_core::slash(p.strip_prefix(&project.root).unwrap_or(p))
}
fn check_plugin_uses(
project: &dre_core::project::Project,
p: &ProjectArgs,
diags: &mut dre_core::Diagnostics,
) {
dre_core::plugins::check_uses(project, diags);
if !p.no_auto_install {
dre_core::manager::explain_undeclared(diags);
}
}
fn report_diags(diags: &dre_core::Diagnostics, printer: &output::Printer) -> bool {
for d in diags.sorted() {
printer.diag(d);
}
if diags.has_errors() {
printer.error(&format!(
"the project has {} error(s); fix them before running (see `dre validate`)",
diags.error_count()
));
return false;
}
true
}
pub(crate) fn run_date() -> Option<chrono::NaiveDate> {
settings::env(settings::RUN_DATE).and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok())
}
pub(crate) fn run_at() -> Result<Option<chrono::DateTime<chrono::Utc>>, String> {
match settings::env(settings::RUN_AT) {
Some(v) => chrono::DateTime::parse_from_rfc3339(&v)
.map(|t| Some(t.with_timezone(&chrono::Utc)))
.map_err(|_| {
format!("DRE_RUN_AT: `{v}` isn't an RFC 3339 date-time (e.g. 2026-09-01T06:00:00Z)")
}),
None => Ok(None),
}
}
fn parse_rate(s: &str) -> Result<dre_core::rate::Rate, String> {
dre_core::rate::Rate::parse(s)
}
fn release(a: ReleaseArgs, printer: output::Printer) -> ExitCode {
if !a.project.no_auto_install && !plugins::sync_packages(&a.project.project_dir, &printer) {
return exit::not_started();
}
let Some((project, _)) = load_for_run(&a.project, &printer) else {
return exit::not_started();
};
if !plugins::ensure(&project, !a.project.no_auto_install, false, &printer) {
return exit::not_started();
}
let by =
a.by.or_else(|| std::env::var("USER").ok())
.or_else(|| std::env::var("USERNAME").ok())
.unwrap_or_else(|| "unknown".into());
let opts = dre_core::release::ReleaseOptions {
report: a.report.clone(),
binding: a.binding.clone(),
run_id: a.run.clone(),
only_keys: a.only_keys,
exclude_keys: a.exclude_keys,
by: by.clone(),
rate: a.rate,
cancel: {
let cancel = dre_core::engine::CancelToken::new();
signals::watch(cancel.clone());
cancel
},
};
let (tx, rx) = std::sync::mpsc::channel();
let events = dre_core::engine::Events::new(tx);
let result = dre_core::release::release(&project, &opts, &events);
drop(events);
for e in rx.try_iter() {
if let dre_core::engine::RunEvent::Warn(m) = e {
printer.line(output::Tone::Warn, "Warning", m.trim());
}
}
match result {
Err(e) => {
printer.error(&e.message);
exit::failed()
}
Ok(o) => {
let n = |s: &str| o.released.iter().filter(|r| r.status == s).count();
for r in o.released.iter().filter(|r| r.status == "delivered") {
printer.detail(
output::Tone::Good,
"Released",
&format!("{} → {}", r.id, r.location.as_deref().unwrap_or("")),
);
}
let summary = format!(
"release {} of run {} by {by}: {} delivered, {} failed, {} refused{}",
o.release_id,
a.run,
n("delivered"),
n("failed"),
n("refused"),
if o.already > 0 {
format!(", {} already released", o.already)
} else {
String::new()
}
);
if o.failed() {
printer.line(output::Tone::Bad, "Failed", &summary);
exit::failed()
} else {
printer.line(output::Tone::Good, "Released", &summary);
exit::ok()
}
}
}
}
fn run(a: RunArgs, mut printer: output::Printer) -> ExitCode {
use std::io::IsTerminal;
let cancel = dre_core::engine::CancelToken::new();
signals::watch(cancel.clone());
if !a.project.no_auto_install && !plugins::sync_packages(&a.project.project_dir, &printer) {
return exit::not_started();
}
let Some((project, manifest_checksum)) = load_for_run(&a.project, &printer) else {
return exit::not_started();
};
if let Some(path) = &a.output_json {
let value = path.to_string_lossy();
if let Err(error) = dre_core::target::resolve(&project.root, Some(&value), None) {
printer.error(&format!("--output-json: {error}"));
return exit::not_started();
}
}
printer.log_runs();
for s in project.settings.lines() {
printer.detail(output::Tone::Note, "Setting", &s);
}
if !plugins::ensure(&project, !a.project.no_auto_install, false, &printer) {
return exit::not_started();
}
let run_timeout = match dre_core::settings::flag_or_env(
a.timeout.as_deref(),
"--timeout",
dre_core::settings::RUN_TIMEOUT,
) {
Some((v, from)) => match dre_protocol::delivery::parse_duration(&serde_json::Value::String(v)) {
Ok(d) if !d.is_zero() => Some(d),
_ => {
printer.error(&format!(
"{from} must be a duration such as `2h` or `90m`, or seconds"
));
return exit::not_started();
}
},
None => project.run_timeout,
};
let keep_runs = match keep_runs_setting(a.keep_runs.as_deref()) {
Ok(k) => k,
Err(e) => {
printer.error(&e);
return exit::not_started();
}
};
let row_concurrency = match dre_core::settings::flag_or_env(
a.row_concurrency.as_deref(),
"--row-concurrency",
dre_core::settings::ROW_CONCURRENCY,
) {
None => None,
Some((v, from)) => match v.trim().parse::<usize>() {
Ok(n) if n > 0 => Some(n),
_ => {
printer.error(&format!("{from} must be a whole number of 1 or more, got `{v}`"));
return exit::not_started();
}
},
};
let threads =
match dre_core::settings::flag_or_env(a.threads.as_deref(), "--threads", dre_core::settings::THREADS)
{
None => None,
Some((v, from)) => match v.trim().parse::<usize>() {
Ok(n) if n > 0 => Some(n),
_ => {
printer.error(&format!("{from} must be a whole number of 1 or more, got `{v}`"));
return exit::not_started();
}
},
};
let mut diags = dre_core::Diagnostics::default();
check_plugin_uses(&project, &a.project, &mut diags);
if !diags.has_errors() {
dre_core::options::check(&project, false, &mut diags);
}
if !report_diags(&diags, &printer) {
return exit::not_started();
}
let opts = dre_core::run::RunOptions {
selector: selection(&a.select, &a.selector),
set: a.set,
target: a.project.target.clone(),
profile: a.profile,
vars: a.project.vars.iter().cloned().collect(),
output_name: a.output_name,
output_path: a.output_path,
dry_run: a.dry_run,
no_deliver: a.no_deliver || a.from_results.is_some(),
output_json: a.output_json,
from_results: a.from_results,
row_concurrency,
hold: a.hold,
retry_failed: a.retry_failed,
only_keys: a.only_keys,
limit_rows: a.limit_rows,
unmask: a.unmask,
rate: a.rate,
preview: a.preview,
accept_schema_change: a.accept_schema_change,
interactive: std::io::stdin().is_terminal() && std::io::stderr().is_terminal(),
date: run_date(),
scheduled_at: run_at().ok().flatten(),
timezone: a.project.timezone(),
live_check: false,
schedule: a.schedule,
manifest_checksum,
cancel,
keep_runs,
threads,
};
if let Some(name) = &opts.schedule
&& !project.schedules.iter().any(|e| &e.name == name)
{
printer.error(&dre_core::run::unknown_schedule(&project, name));
return exit::not_started();
}
let date = opts
.date
.or(opts.scheduled_at.map(|t| t.date_naive()))
.unwrap_or_else(|| chrono::Utc::now().date_naive());
let mut params = serde_json::to_value(opts.params(date)).unwrap_or_default();
params["target"] = serde_json::json!(project.target_name);
params["profiles"] = serde_json::json!(project.profiles.path);
params["target_path"] = serde_json::json!(project.target_dir);
printer.log_params(¶ms);
printer.detail(output::Tone::Note, "Profiles", &profiles_line(&project.profiles));
signals::set_timeout(run_timeout);
if let Some(reason) = opts.cancel.reason() {
printer.error("the run was cancelled before it started");
return ExitCode::from(reason.exit_code());
}
let summary = dre_core::run::run(&project, &opts, &mut printer);
if let Some(e) = &summary.error {
printer.error(e);
return ExitCode::from(summary.exit_code());
}
if let Some(reason) = summary.cancelled {
let what = match reason {
dre_core::engine::CancelReason::Interrupt => "was cancelled by Ctrl-C".to_string(),
dre_core::engine::CancelReason::Terminate => "was cancelled by a termination signal".to_string(),
dre_core::engine::CancelReason::Timeout => {
let during = summary
.outcomes
.iter()
.find(|o| o.status == dre_core::run::Status::TimedOut)
.map(|o| match &o.set {
Some(set) => format!(" while running {} (Set {set})", o.report),
None => format!(" while running {}", o.report),
})
.unwrap_or_default();
format!(
"timed out after {}{during}",
run_timeout.map(signals::human).unwrap_or_default()
)
}
};
let not_run = match summary.not_run {
0 => String::new(),
1 => "; 1 Binding didn't run".into(),
n => format!("; {n} Bindings didn't run"),
};
printer.error(&format!("the run {what}{not_run}"));
}
printer.finish("run");
ExitCode::from(summary.exit_code())
}
fn clean(a: CleanArgs) -> ExitCode {
let root = &a.project_dir;
let from_file = dre_core::target::project_value(root);
let t = match dre_core::target::resolve(root, a.target_path.as_deref(), from_file.as_deref()) {
Ok(t) => t,
Err(e) => {
eprintln!("error: {e}");
return exit::not_started();
}
};
if a.prune {
let keep = match keep_runs_setting(a.keep_runs.as_deref()) {
Ok(Some(k)) => k,
Ok(None) => {
dre_core::target::project_flag_u64(root, "keep_runs").map_or(1, |k| k.max(1) as usize)
}
Err(e) => {
eprintln!("error: {e}");
return exit::not_started();
}
};
let mut removed = 0;
for report in std::fs::read_dir(t.dir.join("run"))
.into_iter()
.flatten()
.flatten()
{
for binding in std::fs::read_dir(report.path()).into_iter().flatten().flatten() {
if binding.path().is_dir() {
removed += dre_core::runs::BindingRuns::new(&binding.path())
.prune(keep)
.len();
}
}
}
eprintln!("Removed {removed} run(s); kept the last {keep} of each report and Set");
return exit::ok();
}
let shown = dre_core::slash(t.dir.strip_prefix(root).unwrap_or(&t.dir));
match dre_core::target::clean_dir(root, &t) {
Ok(dre_core::target::Cleaned::Removed) => {
eprintln!("Removed {}/", shown.display());
exit::ok()
}
Ok(dre_core::target::Cleaned::Missing) => {
eprintln!("Nothing to clean: no {}/ directory", shown.display());
exit::ok()
}
Err(e) => {
eprintln!("error: {e}");
exit::failed()
}
}
}
fn keep_runs_setting(flag: Option<&str>) -> Result<Option<usize>, String> {
match dre_core::settings::flag_or_env(flag, "--keep-runs", dre_core::settings::KEEP_RUNS) {
None => Ok(None),
Some((v, from)) => match v.trim().parse::<usize>() {
Ok(n) if n > 0 => Ok(Some(n)),
_ => Err(format!("{from} must be a whole number of 1 or more, got `{v}`")),
},
}
}
fn deps(a: DepsArgs, printer: &output::Printer) -> ExitCode {
if !plugins::sync_packages(&a.project_dir, printer) {
return exit::failed();
}
let opts = LoadOptions {
profiles_dir: a.profiles_dir,
..Default::default()
};
let (project, diags) = project::load(&a.project_dir, &opts);
let Some(project) = project else {
for d in diags.sorted() {
printer.diag(d);
}
return exit::not_started();
};
if plugins::ensure(&project, true, true, printer) {
let extensions = match extensions::sync_declared(&a.project_dir, a.accept_proprietary, printer) {
Ok(count) => count,
Err(error) => {
printer.error(&error);
return exit::failed();
}
};
printer.line(
output::Tone::Good,
"Synced",
&format!(
"{} plugin(s), {} package(s) and {} companion(s); plugin/package dre.lock is up to date",
project.plugins.len(),
project.packages.len(),
extensions
),
);
exit::ok()
} else {
exit::failed()
}
}
#[cfg(test)]
mod cli_reference_tests {
use clap::CommandFactory;
#[test]
fn the_cli_reference_page_is_current() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/cli-reference.md");
let page = crate::cli_reference::page(super::Cli::command());
let old = std::fs::read_to_string(&path).unwrap_or_default();
let (body, nav) = match old.find("\n<!-- docs-nav") {
Some(i) => (&old[..=i], &old[i + 1..]),
None => (old.as_str(), ""),
};
if std::env::var_os("DRE_UPDATE_DOCS").is_some() {
let nav = if nav.is_empty() {
String::new()
} else {
format!("\n{nav}")
};
std::fs::write(&path, format!("{page}{nav}")).unwrap();
} else {
assert_eq!(
format!("{}\n", body.trim_end()),
page,
"docs/cli-reference.md is stale: run `DRE_UPDATE_DOCS=1 cargo test -p dre-cli --bin dre cli_reference`"
);
}
}
}