Skip to main content

agent_first_psql/
skill_admin.rs

1use crate::cli::{
2    SkillAdminAction, SkillAdminOptions, SkillAdminRequest, SkillAgentSelection, SkillScope,
3};
4use agent_first_data::skill::{
5    self, SkillAction, SkillAgentSelection as AfSelection, SkillOptions, SkillScope as AfScope,
6    SkillSpec,
7};
8use serde_json::Value;
9
10const SPEC: SkillSpec = SkillSpec {
11    name: "agent-first-psql",
12    source: include_str!("../skills/agent-first-psql/SKILL.md"),
13    title: "Agent-First PSQL",
14    marker_slug: "afpsql",
15    assets: &[],
16};
17
18pub fn run(req: SkillAdminRequest) -> i32 {
19    let (action, options) = split_action(req.action);
20    let mut emitter =
21        agent_first_data::CliEmitter::from_output_to(crate::emit::output_to(), req.output)
22            .with_strict_protocol();
23    match skill::run_skill_admin(&SPEC, action, &options) {
24        Ok(report) => match serde_json::to_value(&report) {
25            Ok(value) => match emitter.emit_result(value) {
26                Ok(()) => 0,
27                Err(_) => 4,
28            },
29            Err(error) => match emitter.emit_error(
30                "serialization_failed",
31                &format!("failed to serialize skill report: {error}"),
32            ) {
33                Ok(()) => 1,
34                Err(_) => 4,
35            },
36        },
37        Err(err) => match agent_first_data::json_error("cli_error", &err.message)
38            .hint_if_some(err.hint.as_deref())
39            .field(
40                "partial_report",
41                err.partial_report
42                    .and_then(|report| serde_json::to_value(report).ok())
43                    .unwrap_or(Value::Null),
44            )
45            .build()
46            .map_err(agent_first_data::CliEmitterError::Build)
47        {
48            Ok(event) => match emitter.emit(event) {
49                Ok(()) => 1,
50                Err(_) => 4,
51            },
52            Err(_) => 4,
53        },
54    }
55}
56
57fn split_action(action: SkillAdminAction) -> (SkillAction, SkillOptions) {
58    match action {
59        SkillAdminAction::Status(options) => (SkillAction::Status, convert_options(options)),
60        SkillAdminAction::Install(options) => (SkillAction::Install, convert_options(options)),
61        SkillAdminAction::Uninstall(options) => (SkillAction::Uninstall, convert_options(options)),
62    }
63}
64
65fn convert_options(options: SkillAdminOptions) -> SkillOptions {
66    SkillOptions {
67        agent: convert_agent(options.agent),
68        scope: convert_scope(options.scope),
69        skills_dir: options.skills_dir,
70        force: options.force,
71    }
72}
73
74fn convert_agent(agent: SkillAgentSelection) -> AfSelection {
75    match agent {
76        SkillAgentSelection::All => AfSelection::All,
77        SkillAgentSelection::Codex => AfSelection::Codex,
78        SkillAgentSelection::ClaudeCode => AfSelection::ClaudeCode,
79        SkillAgentSelection::Opencode => AfSelection::Opencode,
80        SkillAgentSelection::Hermes => AfSelection::Hermes,
81    }
82}
83
84fn convert_scope(scope: SkillScope) -> AfScope {
85    match scope {
86        SkillScope::Personal => AfScope::Personal,
87        SkillScope::Workspace => AfScope::Workspace,
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::SPEC;
94
95    #[test]
96    fn shell_guidance_protects_placeholder_dollars() {
97        assert!(
98            SPEC.source
99                .contains("quote SQL containing `$1..$N` placeholders with single")
100        );
101        assert!(SPEC.source.contains("shells expand `$1` and `$2`"));
102        assert!(SPEC.source.contains("`--sql-file` / pipe mode JSON"));
103    }
104}