use clap::Args as ClapArgs;
use influxdb3_plugin_schemas::Index;
use influxdb3_plugin_schemas::ValidationError;
use influxdb3_plugin_sdk::{SdkError, package};
use std::path::PathBuf;
use crate::cli_error::CliError;
use crate::color::Stream;
use crate::output::error_mapping::{ErrorContext, json_error_from_sdk, json_error_from_validation};
use crate::output::json::{JsonError, PackageOutput, write_envelope_ok};
use crate::output::{Env, OutputMode, RealEnv, resolve_output_mode};
use crate::path_display::{absolutize_for_json, display_relative_to_cwd, paths_overlap};
use crate::style::Palette;
#[derive(Debug, ClapArgs)]
pub(crate) struct Args {
#[arg(default_value = ".")]
plugin_dir: PathBuf,
#[arg(long, value_enum)]
output: Option<OutputMode>,
#[arg(long)]
index: PathBuf,
#[arg(long)]
out: PathBuf,
}
impl Args {
pub(crate) fn run(self) -> anyhow::Result<()> {
run_with_env(self, &RealEnv)
}
}
fn run_with_env(args: Args, env: &dyn Env) -> anyhow::Result<()> {
let mode = resolve_output_mode(args.output, env);
let stdout_palette = Palette::for_stream(Stream::Stdout, mode, env, env.stdout_is_terminal());
let index_path = absolutize_for_json(&args.index)?;
let out_path = absolutize_for_json(&args.out)?;
let index_display = index_path.display().to_string();
let out_display = out_path.display().to_string();
let index_raw = std::fs::read_to_string(&args.index).map_err(|e| {
CliError::runtime(JsonError {
code: "io::read_failed".into(),
message: format!("failed to read --index {index_display}: {e}"),
field: Some(index_display.clone()),
details: Some(serde_json::json!({
"path": index_display,
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
let input_index = Index::parse_json(&index_raw).map_err(|schema_errors| {
let diagnostics: Vec<JsonError> = schema_errors
.into_iter()
.map(|reported| json_error_from_validation(&ValidationError::SchemaReported(reported)))
.collect();
CliError::runtime(JsonError {
code: "package::index_parse_failed".into(),
message: format!("failed to parse --index {index_display} as a registry index"),
field: Some(index_display.clone()),
details: None,
diagnostics,
cause: vec![],
})
})?;
std::fs::create_dir_all(&args.out).map_err(|e| {
CliError::runtime(JsonError {
code: "io::write_failed".into(),
message: format!("failed to create --out {out_display}: {e}"),
field: Some(out_display.clone()),
details: Some(serde_json::json!({
"path": out_display,
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
if paths_overlap(&args.index, &args.out, &index_display, &out_display)? {
return Err(CliError::usage(JsonError {
code: "usage::input_output_overlap".into(),
message: format!(
"--out {} resolves to the directory containing --index {}; \
this would overwrite the input index. Use a different --out directory.",
out_display, index_display,
),
field: None,
details: Some(serde_json::json!({
"index": index_display,
"out": out_display,
})),
diagnostics: vec![],
cause: vec![],
}));
}
let outcome = match package::package_plugin(&args.plugin_dir, input_index) {
Ok(o) => o,
Err(ref e @ SdkError::AlreadyPublished { .. }) => {
return Err(CliError::runtime(json_error_from_sdk(
e,
ErrorContext::Package,
)));
}
Err(ref e @ SdkError::CanonicalCollision { .. }) => {
return Err(CliError::runtime(json_error_from_sdk(
e,
ErrorContext::Package,
)));
}
Err(SdkError::ValidationErrors(errs)) => {
return Err(validation_errors_to_cli_error(errs));
}
Err(other) => {
return Err(CliError::runtime(json_error_from_sdk(
&other,
ErrorContext::Package,
)));
}
};
let artifact_filename = format!(
"{}-{}.tar.gz",
outcome.new_entry.name.as_str(),
outcome.new_entry.version,
);
let artifact_path = out_path.join(&artifact_filename);
let derived_index_path = out_path.join("index.json");
let derived_index_json = outcome.derived_index.to_canonical_json().map_err(|e| {
let sdk_err = SdkError::from(e);
CliError::runtime(json_error_from_sdk(&sdk_err, ErrorContext::Package))
})?;
std::fs::write(&artifact_path, &outcome.archive_bytes).map_err(|e| {
CliError::runtime(JsonError {
code: "io::write_failed".into(),
message: format!("failed to write artifact {}: {e}", artifact_path.display()),
field: Some(artifact_path.display().to_string()),
details: Some(serde_json::json!({
"path": artifact_path.display().to_string(),
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
std::fs::write(&derived_index_path, &derived_index_json).map_err(|e| {
CliError::runtime(JsonError {
code: "io::write_failed".into(),
message: format!(
"failed to write derived index {}: {e}",
derived_index_path.display()
),
field: Some(derived_index_path.display().to_string()),
details: Some(serde_json::json!({
"path": derived_index_path.display().to_string(),
"io_kind": format!("{:?}", e.kind()),
})),
diagnostics: vec![],
cause: vec![e.to_string()],
})
})?;
let payload = PackageOutput {
artifact_path,
index_path: derived_index_path,
hash: outcome.hash.as_str().to_owned(),
new_entry_name: outcome.new_entry.name.as_str().to_owned(),
new_entry_version: outcome.new_entry.version.to_string(),
new_entry_published_at: outcome.new_entry.published_at.to_string(),
};
match mode {
OutputMode::Human => {
render_human(&payload, stdout_palette, &mut std::io::stdout())?;
}
OutputMode::Json => {
write_envelope_ok(&mut std::io::stdout(), &payload)?;
}
}
Ok(())
}
fn render_human(
payload: &PackageOutput,
palette: Palette,
writer: &mut impl std::io::Write,
) -> std::io::Result<()> {
let ok = palette.success.render();
let ok_reset = palette.success.render_reset();
writeln!(
writer,
"{ok}Packaged {}@{}{ok_reset}",
payload.new_entry_name, payload.new_entry_version
)?;
writeln!(
writer,
" artifact: {}",
display_relative_to_cwd(&payload.artifact_path)
)?;
writeln!(
writer,
" index: {}",
display_relative_to_cwd(&payload.index_path)
)?;
writeln!(writer, " hash: {}", payload.hash)?;
Ok(())
}
fn validation_errors_to_cli_error(errs: Vec<ValidationError>) -> anyhow::Error {
let je = JsonError {
code: "validate::failed".into(),
message: format!("{} validation error(s) found", errs.len()),
field: None,
details: None,
diagnostics: errs.iter().map(json_error_from_validation).collect(),
cause: vec![],
};
CliError::runtime(je)
}