use std::path::PathBuf;
use clap::Args;
use shepherd::registry::{Registry, RegistryRepairRequest, RegistryRollbackRequest};
use crate::{
ContextInputs, ExecutionContext,
interface::{CliError, CliGlobals},
};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Args)]
#[command(disable_help_subcommand = true)]
pub struct RegistryCmd {
#[command(subcommand)]
action: Option<RegistryAction>,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Subcommand)]
enum RegistryAction {
Repair {
#[arg(long)]
confirm: bool,
#[arg(long, value_name = "PATH")]
snapshot_dir: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Rollback {
#[arg(long, value_name = "PATH")]
receipt: PathBuf,
#[arg(long, value_name = "SHA256")]
witness_sha256: String,
#[arg(long, value_name = "SHA256")]
receipt_sha256: String,
#[arg(long)]
confirm: bool,
#[arg(long)]
json: bool,
},
}
impl RegistryCmd {
pub(crate) fn run(self, globals: CliGlobals) -> Result<(), CliError> {
let mut context = context(globals)?;
match self.action {
Some(RegistryAction::Repair {
confirm,
snapshot_dir,
json,
}) => {
if !confirm {
return Err(CliError::message_with_code(
"registry repair is mutating; re-run with --confirm",
2,
));
}
let report = Registry::repair(&RegistryRepairRequest {
registry_path: context.registry_path.clone(),
project_root: context.primary_root.clone(),
snapshot_dir,
})
.map_err(|error| CliError::message_with_code(error.to_string(), 5))?;
if json {
write_report_json(&mut context, &report)?;
} else {
write(
&mut context,
format!(
"registry repaired: {}\nsnapshot: {}\nreceipt: {}\nplan witness: {}\nreceipt witness: {}\nrollback: {}",
report.receipt.project_id,
report.snapshot_path.display(),
report.receipt_path.display(),
report.receipt.plan_sha256,
report.receipt_sha256,
report.rollback_command
),
)?;
}
Ok(())
}
Some(RegistryAction::Rollback {
receipt,
witness_sha256,
receipt_sha256,
confirm,
json,
}) => {
if !confirm {
return Err(CliError::message_with_code(
"registry rollback is mutating; re-run with --confirm",
2,
));
}
let report = Registry::rollback(&RegistryRollbackRequest {
receipt_path: receipt,
project_root: context.primary_root.clone(),
witness_sha256,
receipt_sha256,
})
.map_err(|error| CliError::message_with_code(error.to_string(), 5))?;
if json {
write_report_json(&mut context, &report)?;
} else {
write(
&mut context,
format!(
"registry rolled back: {}\nsnapshot: {}\nreceipt: {}\nplan witness: {}\nreceipt witness: {}\nrollback: {}",
report.receipt.project_id,
report.snapshot_path.display(),
report.receipt_path.display(),
report.receipt.plan_sha256,
report.receipt_sha256,
report.rollback_command
),
)?;
}
Ok(())
}
None => write(&mut context, "shepherd registry <repair|rollback>".into()),
}
}
}
fn context(globals: CliGlobals) -> Result<ExecutionContext, CliError> {
let cwd = std::env::current_dir().map_err(|error| CliError::message(error.to_string()))?;
let mut inputs = ContextInputs::from_environment(cwd)
.map_err(|error| CliError::message(error.to_string()))?;
inputs.explicit_config = globals.config;
inputs.verbosity = globals.verbosity;
ExecutionContext::discover(inputs).map_err(|error| CliError::message(error.to_string()))
}
fn write(context: &mut ExecutionContext, output: String) -> Result<(), CliError> {
context
.write_stdout(format!("{output}\n").as_bytes())
.map_err(|error| CliError::message(format!("cannot write stdout: {error}")))
}
fn write_json<T: serde::Serialize>(
context: &mut ExecutionContext,
value: &T,
) -> Result<(), CliError> {
let output = serde_json::to_string_pretty(value)
.map_err(|error| CliError::message(format!("cannot encode registry evidence: {error}")))?;
write(context, output)
}
fn write_report_json(
context: &mut ExecutionContext,
report: &shepherd::registry::RegistryRepairReport,
) -> Result<(), CliError> {
let mut value = serde_json::to_value(&report.receipt)
.map_err(|error| CliError::message(format!("cannot encode registry evidence: {error}")))?;
value["receipt_sha256"] = serde_json::Value::String(report.receipt_sha256.clone());
value["rollback_command"] = serde_json::Value::String(report.rollback_command.clone());
write_json(context, &value)
}