use crate::catalog::CatalogItem;
use crate::cli::HookEventArg;
use crate::error::{MindError, Result};
use crate::manifest::{InstalledItem, Manifest};
use crate::mindfile::{HookEvent, MindToml, ResolvedHook};
use crate::paths::Paths;
use crate::resolve::{HookTarget, parse_hook_target, select_installed, source_matches_glob};
use crate::source::{Pin, RecordedHook, Registry, Source};
pub fn run(
paths: &Paths,
target: &str,
event: HookEventArg,
force: bool,
dangerously_skip_install: bool,
dangerously_skip_build: bool,
) -> Result<()> {
match parse_hook_target(target)? {
HookTarget::Source(selector) => {
if event == HookEventArg::Build {
return Err(MindError::BuildEventRequiresItemTarget);
}
run_source_hooks(paths, &selector, event, force, dangerously_skip_install)
}
HookTarget::Item(item_ref) => run_item_hooks(
paths,
&item_ref,
event,
dangerously_skip_install,
dangerously_skip_build,
),
}
}
pub fn list(paths: &Paths, target: &str) -> Result<()> {
match parse_hook_target(target)? {
HookTarget::Source(selector) => list_source_hooks(paths, &selector),
HookTarget::Item(item_ref) => list_item_hooks(paths, &item_ref),
}
}
fn run_source_hooks(
paths: &Paths,
selector: &str,
event: HookEventArg,
force: bool,
dangerously_skip: bool,
) -> Result<()> {
let mut registry = Registry::load(paths)?;
let indices: Vec<usize> = registry
.sources
.iter()
.enumerate()
.filter(|(_, s)| source_matches_glob(&s.name, selector))
.map(|(i, _)| i)
.collect();
if indices.is_empty() {
return Err(MindError::SourceNotFound {
name: selector.to_string(),
});
}
let mut registry_dirty = false;
for idx in indices {
let source = ®istry.sources[idx];
let clone_dir = source.clone_dir(paths);
let toml_path = clone_dir.join("mind.toml");
let mindfile = MindToml::load(&clone_dir).unwrap_or_default();
let resolved = mindfile
.as_ref()
.map(|m| m.resolved_hooks(&toml_path))
.transpose()?
.unwrap_or_default();
let hook_event = match event {
HookEventArg::Install => HookEvent::Install,
HookEventArg::Uninstall => HookEvent::Uninstall,
HookEventArg::Build => unreachable!("caller guards build for source targets"),
};
let event_name = event_label(event);
let hooks: Vec<&ResolvedHook> = resolved.iter().filter(|h| h.event == hook_event).collect();
if hooks.is_empty() {
println!(
"note: no {event_name} hooks declared for source {}",
source.name
);
continue;
}
let source_name = source.name.clone();
let pin_desc = pin_description(&source.pin);
let commit = source.commit.clone().unwrap_or_default();
let current = source.commit.clone();
let clone_path = clone_dir.display().to_string();
let browse_url = source.browse_url(&commit);
for h in &hooks {
if hook_event == HookEvent::Install
&& !force
&& hook_already_ran(®istry.sources[idx], &h.run, current.as_deref())
{
continue;
}
let disclosure = crate::hook::hook_disclosure_text(
h.label(),
h.optional,
&source_name,
&pin_desc,
&commit,
&clone_path,
&h.run,
None,
browse_url.as_deref(),
);
match crate::hook::decide(&disclosure, h.optional, dangerously_skip)? {
crate::hook::HookAct::Run => {
println!(
"running {event_name} hook '{}' for {}",
h.label(),
source_name
);
if let Err(e) =
crate::hook::run_hook(&h.run, &clone_dir, &source_name, h.label())
{
if registry_dirty {
let _ = registry.save(paths);
}
return Err(e);
}
if hook_event == HookEvent::Install {
record_hook_run(&mut registry.sources[idx], &h.run, current.clone());
registry_dirty = true;
}
}
crate::hook::HookAct::Skip => {
println!(
"note: skipped {event_name} hook '{}' for {}",
h.label(),
source_name
);
if hook_event == HookEvent::Install {
record_hook_run(&mut registry.sources[idx], &h.run, None);
registry_dirty = true;
}
}
crate::hook::HookAct::Abort => {
if registry_dirty {
let _ = registry.save(paths);
}
return Err(MindError::HookAborted {
label: h.label().to_string(),
});
}
}
}
}
if registry_dirty {
registry.save(paths)?;
}
Ok(())
}
fn run_item_hooks(
paths: &Paths,
item_ref: &crate::resolve::ItemRef,
event: HookEventArg,
dangerously_skip_install: bool,
dangerously_skip_build: bool,
) -> Result<()> {
let manifest = Manifest::load(paths)?;
let matches = select_installed(&manifest.items, item_ref);
if matches.is_empty() {
return Err(MindError::NotInstalled {
name: item_ref.name.clone(),
});
}
for installed in matches {
match event {
HookEventArg::Build => {
run_item_build(paths, installed, dangerously_skip_build)?;
}
HookEventArg::Install | HookEventArg::Uninstall => {
run_item_lifecycle_hooks(paths, installed, event, dangerously_skip_install)?;
}
}
}
Ok(())
}
fn run_item_build(
paths: &Paths,
installed: &InstalledItem,
dangerously_skip_build: bool,
) -> Result<()> {
let registry = Registry::load(paths)?;
let source = registry
.sources
.iter()
.find(|s| s.name == installed.source)
.ok_or_else(|| MindError::SourceNotFound {
name: installed.source.clone(),
})?;
let mut catalog_items = Vec::new();
crate::catalog::scan_source(paths, source, &mut catalog_items)?;
let catalog_item = catalog_items
.iter()
.find(|c| c.kind == installed.kind && c.name == installed.bare_name)
.ok_or_else(|| MindError::NotInstalled {
name: installed.name.clone(),
})?;
let commit = installed.commit.clone();
println!("rebuilding {} via transactional reinstall", installed.key());
let new_installed = crate::install::install(
paths,
catalog_item,
&commit,
&catalog_items,
false,
dangerously_skip_build,
)?;
let mut manifest = Manifest::load(paths)?;
manifest.insert(new_installed);
manifest.save(paths)?;
Ok(())
}
fn run_item_lifecycle_hooks(
paths: &Paths,
installed: &InstalledItem,
event: HookEventArg,
dangerously_skip: bool,
) -> Result<()> {
let registry = Registry::load(paths)?;
let source = registry
.sources
.iter()
.find(|s| s.name == installed.source)
.ok_or_else(|| MindError::SourceNotFound {
name: installed.source.clone(),
})?;
let mut catalog_items: Vec<CatalogItem> = Vec::new();
crate::catalog::scan_source(paths, source, &mut catalog_items)?;
let catalog_item = catalog_items
.iter()
.find(|c| c.kind == installed.kind && c.name == installed.bare_name)
.ok_or_else(|| MindError::NotInstalled {
name: installed.name.clone(),
})?;
let store = paths.mind_home.join(&installed.store);
let commit = &installed.commit;
match event {
HookEventArg::Install => {
let hooks = catalog_item.install_hooks();
if hooks.is_empty() {
println!("note: no install hooks declared for {}", installed.key());
return Ok(());
}
crate::install::run_item_install_hooks(
catalog_item,
&hooks,
&store,
commit,
dangerously_skip,
)?;
}
HookEventArg::Uninstall => {
let hooks = catalog_item.uninstall_hooks();
if hooks.is_empty() {
println!("note: no uninstall hooks declared for {}", installed.key());
return Ok(());
}
crate::install::run_item_uninstall_hooks(
installed,
&hooks,
&store,
commit,
dangerously_skip,
)?;
}
HookEventArg::Build => unreachable!("build handled by run_item_build"),
}
Ok(())
}
fn list_source_hooks(paths: &Paths, selector: &str) -> Result<()> {
let registry = Registry::load(paths)?;
let manifest = Manifest::load(paths)?;
let matching: Vec<&Source> = registry
.sources
.iter()
.filter(|s| source_matches_glob(&s.name, selector))
.collect();
if matching.is_empty() {
return Err(MindError::SourceNotFound {
name: selector.to_string(),
});
}
for source in matching {
let clone_dir = source.clone_dir(paths);
let toml_path = clone_dir.join("mind.toml");
let mindfile = MindToml::load(&clone_dir).unwrap_or_default();
let resolved = mindfile
.as_ref()
.map(|m| m.resolved_hooks(&toml_path))
.transpose()?
.unwrap_or_default();
println!("source: {}", source.name);
let current = source.commit.as_deref();
if resolved.is_empty() {
println!(" (no source-level hooks declared)");
} else {
for h in &resolved {
let kind_str = if h.optional { "optional" } else { "required" };
let event_str = match h.event {
HookEvent::Install => "install",
HookEvent::Uninstall => "uninstall",
};
let status = if h.event == HookEvent::Install {
install_hook_status(source, &h.run, current)
} else {
"(not recorded)".to_string()
};
println!(" [{event_str}] {kind_str} {:?} {}", h.run, status);
}
}
let source_items: Vec<&InstalledItem> = manifest
.items
.values()
.filter(|it| it.source == source.name)
.collect();
if !source_items.is_empty() {
let mut catalog_items = Vec::new();
let _ = crate::catalog::scan_source(paths, source, &mut catalog_items);
for installed in source_items {
let item_hooks: Vec<_> = if let Some(c) = catalog_items
.iter()
.find(|c| c.kind == installed.kind && c.name == installed.bare_name)
{
c.hooks.iter().collect()
} else {
vec![]
};
if !item_hooks.is_empty() {
println!(" item: {}", installed.key());
for h in item_hooks {
let kind_str = if h.optional { "optional" } else { "required" };
let event_str = match h.event {
HookEvent::Install => "install",
HookEvent::Uninstall => "uninstall",
};
println!(" [{event_str}] {kind_str} {:?}", h.run);
}
}
}
}
}
Ok(())
}
fn list_item_hooks(paths: &Paths, item_ref: &crate::resolve::ItemRef) -> Result<()> {
let manifest = Manifest::load(paths)?;
let matches = select_installed(&manifest.items, item_ref);
if matches.is_empty() {
return Err(MindError::NotInstalled {
name: item_ref.name.clone(),
});
}
let registry = Registry::load(paths)?;
for installed in matches {
println!("item: {} (source {})", installed.key(), installed.source);
let source = registry.sources.iter().find(|s| s.name == installed.source);
let hooks: Vec<ResolvedHook> = if let Some(src) = source {
let mut catalog_items = Vec::new();
let _ = crate::catalog::scan_source(paths, src, &mut catalog_items);
catalog_items
.iter()
.find(|c| c.kind == installed.kind && c.name == installed.bare_name)
.map(|c| c.hooks.clone())
.unwrap_or_default()
} else {
vec![]
};
if hooks.is_empty() {
println!(" (no hooks declared)");
} else {
for h in &hooks {
let kind_str = if h.optional { "optional" } else { "required" };
let event_str = match h.event {
HookEvent::Install => "install",
HookEvent::Uninstall => "uninstall",
};
println!(" [{event_str}] {kind_str} {:?}", h.run);
}
}
}
Ok(())
}
fn pin_description(pin: &Pin) -> String {
match pin {
Pin::DefaultBranch => "default branch".to_string(),
Pin::FollowBranch(b) => format!("branch {b}"),
Pin::Tag(t) => format!("tag {t}"),
Pin::Ref(r) => format!("ref {r}"),
}
}
fn hook_already_ran(source: &Source, command: &str, current: Option<&str>) -> bool {
current.is_some()
&& source
.install_hooks
.iter()
.any(|r| r.command == command && r.ran_at.as_deref() == current)
}
fn record_hook_run(source: &mut Source, command: &str, ran_at: Option<String>) {
if let Some(r) = source
.install_hooks
.iter_mut()
.find(|r| r.command == command)
{
r.ran_at = ran_at;
} else {
source.install_hooks.push(RecordedHook {
command: command.to_string(),
ran_at,
});
}
}
fn event_label(event: HookEventArg) -> &'static str {
match event {
HookEventArg::Install => "install",
HookEventArg::Uninstall => "uninstall",
HookEventArg::Build => "build",
}
}
fn install_hook_status(source: &Source, command: &str, current: Option<&str>) -> String {
match source.install_hooks.iter().find(|r| r.command == command) {
None => "pending (never ran)".to_string(),
Some(RecordedHook { ran_at: None, .. }) => "pending (never ran)".to_string(),
Some(RecordedHook {
ran_at: Some(ran), ..
}) => {
if current.is_some_and(|c| c == ran) {
format!("ran at {ran}")
} else {
format!("pending (last ran at {ran})")
}
}
}
}