Skip to main content

systemprompt_cli/commands/core/hooks/
list.rs

1//! `core hooks list` command scanning the hooks directory.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}