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