Skip to main content

sac/commands/
registry.rs

1use super::*;
2use super::template::expand_command_template;
3
4#[derive(Clone)]
5pub struct CommandRegistry {
6    commands: Arc<HashMap<String, CommandRecord>>,
7}
8
9impl CommandRegistry {
10    pub fn load(workspace_dir: Option<&Path>) -> Option<Arc<Self>> {
11        let sources = discover_command_sources(workspace_dir);
12        let mut commands = HashMap::new();
13
14        for source in sources {
15            let files = discover_command_files(&source.root);
16
17            for (name, path) in files {
18                if commands.contains_key(&name) {
19                    tracing::debug!(
20                        command = %name,
21                        path = %path.display(),
22                        "command shadowed by higher-precedence definition"
23                    );
24                    continue;
25                }
26
27                match parse_command_file(&path) {
28                    Ok(Some(parsed)) => {
29                        if parsed.model.is_some() {
30                            tracing::debug!(
31                                command = %name,
32                                model = ?parsed.model,
33                                "command specifies model override (ignored in sac)"
34                            );
35                        }
36                        commands.insert(
37                            name.clone(),
38                            CommandRecord {
39                                name: name.clone(),
40                                description: parsed.description,
41                                agent: parsed.agent,
42                                model: parsed.model,
43                                subtask: parsed.subtask,
44                                template: parsed.template,
45                                source_path: path,
46                            },
47                        );
48                    }
49                    Ok(None) => {
50                        tracing::debug!(
51                            path = %path.display(),
52                            "command file skipped (empty or invalid)"
53                        );
54                    }
55                    Err(error) => {
56                        eprintln!(
57                            "Command file '{}' has errors and will be skipped: {:#}",
58                            path.display(),
59                            error
60                        );
61                    }
62                }
63            }
64        }
65
66        if commands.is_empty() {
67            return None;
68        }
69
70        tracing::info!(count = commands.len(), "loaded custom commands");
71        Some(Arc::new(Self {
72            commands: Arc::new(commands),
73        }))
74    }
75
76    pub fn has_command(&self, name: &str) -> bool {
77        self.commands.contains_key(name)
78    }
79
80    pub fn get(&self, name: &str) -> Option<&CommandRecord> {
81        self.commands.get(name)
82    }
83
84    pub fn catalog_entries(&self) -> Vec<CommandCatalogEntry> {
85        let mut entries: Vec<_> = self
86            .commands
87            .values()
88            .map(|cmd| CommandCatalogEntry {
89                name: cmd.name.clone(),
90                description: cmd.description.clone(),
91            })
92            .collect();
93        entries.sort_by(|a, b| a.name.cmp(&b.name));
94        entries
95    }
96
97    pub fn expand(
98        &self,
99        name: &str,
100        args: &str,
101        working_directory: &Path,
102    ) -> Option<String> {
103        let record = self.commands.get(name)?;
104
105        let expanded = expand_command_template(&record.template, args, working_directory);
106
107        // Wrap in structured envelope for display roundtripping
108        let mut prompt = format!(
109            "# /{}: Custom Command\n\nArguments:\n{}\n\n",
110            name,
111            if args.is_empty() { "(none)" } else { args }
112        );
113
114        if let Some(agent) = &record.agent {
115            prompt.push_str(&format!(
116                "[Skill hint: consider activating the \"{}\" skill if available.]\n\n",
117                agent
118            ));
119        }
120
121        prompt.push_str(&expanded);
122        Some(prompt)
123    }
124}