systemprompt_cli/commands/core/hooks/
list.rs1use anyhow::{Context, Result};
7use clap::Args;
8use std::path::Path;
9
10use crate::CliConfig;
11use crate::shared::CommandOutput;
12use systemprompt_models::{DiskHookConfig, HOOK_CONFIG_FILENAME};
13
14use super::types::{HookEntry, HookListOutput};
15
16#[derive(Debug, Clone, Copy, Args)]
17pub struct ListArgs;
18
19pub(super) fn execute(args: ListArgs, _config: &CliConfig) -> Result<CommandOutput> {
20 let profile = systemprompt_config::ProfileBootstrap::get().context("Failed to get profile")?;
21 let hooks_path = std::path::PathBuf::from(profile.paths.hooks());
22 execute_with_path(args, &hooks_path)
23}
24
25pub fn execute_with_path(_args: ListArgs, hooks_path: &Path) -> Result<CommandOutput> {
26 let hooks = scan_hooks(hooks_path)?;
27 let output = HookListOutput { hooks };
28
29 Ok(CommandOutput::table_of(
30 vec!["plugin_id", "event", "matcher", "hook_type", "command"],
31 &output.hooks,
32 )
33 .with_title("Hooks"))
34}
35
36fn scan_hooks(hooks_path: &Path) -> Result<Vec<HookEntry>> {
37 if !hooks_path.exists() {
38 return Ok(Vec::new());
39 }
40
41 let mut entries = Vec::new();
42
43 for dir_entry in std::fs::read_dir(hooks_path)? {
44 let dir_entry = dir_entry?;
45 let path = dir_entry.path();
46 if !path.is_dir() {
47 continue;
48 }
49
50 let config_path = path.join(HOOK_CONFIG_FILENAME);
51 if !config_path.exists() {
52 continue;
53 }
54
55 let content = match std::fs::read_to_string(&config_path) {
56 Ok(c) => c,
57 Err(e) => {
58 tracing::warn!(path = %config_path.display(), error = %e, "Failed to read hook config");
59 continue;
60 },
61 };
62
63 let config: DiskHookConfig = match serde_yaml::from_str(&content) {
64 Ok(c) => c,
65 Err(e) => {
66 tracing::warn!(path = %config_path.display(), error = %e, "Failed to parse hook config");
67 continue;
68 },
69 };
70
71 let dir_name = path
72 .file_name()
73 .and_then(|n| n.to_str())
74 .unwrap_or("")
75 .to_owned();
76 let id_str = if config.id.as_str().is_empty() {
77 dir_name
78 } else {
79 config.id.as_str().to_owned()
80 };
81
82 entries.push(HookEntry {
83 plugin_id: id_str,
84 event: config.event.as_str().to_owned(),
85 matcher: config.matcher.clone(),
86 hook_type: "command".to_owned(),
87 command: if config.command.is_empty() {
88 None
89 } else {
90 Some(config.command.clone())
91 },
92 });
93 }
94
95 Ok(entries)
96}