use std::sync::Arc;
use serde::Serialize;
use super::{activity::HookActivity, catalog::HookCatalog, dispatch::HookEngine, HookPipeline};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct HookContractView {
pub active: bool,
pub id: String,
pub event: String,
pub tools: String,
pub command: Vec<String>,
pub working_directory: String,
pub timeout: String,
pub environment: Vec<String>,
}
impl std::fmt::Display for HookContractView {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"{}{} on {} (tools: {})\n argv: {}\n cwd: {}\n timeout: {}\n env: {}",
if self.active { "" } else { "[inactive] " },
self.id,
self.event,
self.tools,
self.command.join(" "),
self.working_directory,
self.timeout,
self.environment.join(", ")
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct HookActivityView {
pub hook: String,
pub event: String,
pub outcome: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
pub truncated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl From<&HookActivity> for HookActivityView {
fn from(activity: &HookActivity) -> Self {
Self {
hook: activity.hook_id.clone(),
event: activity.event.to_owned(),
outcome: activity.outcome.label().to_owned(),
duration_ms: activity
.duration
.map(|duration| duration.as_millis() as u64),
truncated: activity.truncated,
detail: activity.outcome.detail().map(str::to_owned),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct HookReport {
pub enabled: bool,
pub files: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skipped_untrusted: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skipped_untrusted_error: Option<String>,
pub hooks: Vec<HookContractView>,
pub recent_activity: Vec<HookActivityView>,
}
impl HookReport {
pub fn disabled() -> Self {
Self {
enabled: false,
files: Vec::new(),
skipped_untrusted: None,
skipped_untrusted_error: None,
hooks: Vec::new(),
recent_activity: Vec::new(),
}
}
pub fn render(&self) -> String {
let mut lines = Vec::new();
if self.files.is_empty() {
lines.push("no hooks files found".to_owned());
} else {
lines.push(format!("hooks files: {}", self.files.join(", ")));
}
if let Some(skipped) = &self.skipped_untrusted {
lines.push(format!(
"ignoring {skipped} because this workspace is not trusted; set {}=1 to load it",
super::TRUST_PROJECT_HOOKS_ENV
));
}
if let Some(error) = &self.skipped_untrusted_error {
lines.push(format!("could not inspect untrusted hooks: {error}"));
}
if self.hooks.is_empty() {
lines.push("no hooks are configured".to_owned());
return lines.join("\n");
}
for hook in &self.hooks {
lines.push(hook.to_string());
}
lines.join("\n")
}
}
#[derive(Clone)]
pub struct HookInspector {
engine: Arc<HookEngine>,
}
impl HookInspector {
pub fn new(runtime: &HookPipeline) -> Self {
Self {
engine: Arc::clone(runtime.engine()),
}
}
pub fn report(&self) -> HookReport {
let catalog = self.engine.catalog();
HookReport {
enabled: true,
files: catalog
.files()
.iter()
.map(|path| crate::paths::display(path))
.collect(),
skipped_untrusted: catalog
.skipped_untrusted()
.map(|skipped| crate::paths::display(&skipped.path)),
skipped_untrusted_error: catalog
.skipped_untrusted()
.and_then(|skipped| skipped.error())
.map(ToString::to_string),
hooks: contract_views(&catalog),
recent_activity: self
.engine
.activity()
.iter()
.map(HookActivityView::from)
.collect(),
}
}
}
impl std::fmt::Debug for HookInspector {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("HookInspector")
.field("hooks", &self.engine.catalog().len())
.finish()
}
}
pub fn contract_views(catalog: &HookCatalog) -> Vec<HookContractView> {
catalog
.spawn_contract()
.into_iter()
.map(|contract| HookContractView {
active: contract.active,
id: contract.id,
event: contract.event.to_owned(),
tools: contract.tools,
command: contract.command,
working_directory: crate::paths::display(&contract.working_directory),
timeout: humantime::format_duration(contract.timeout).to_string(),
environment: contract.environment,
})
.collect()
}
#[cfg(test)]
#[path = "diagnostics_tests.rs"]
mod tests;