caliban_plugins/
expand.rs1use std::path::Path;
12
13use caliban_common::expand::{ExpandContext, MissingPolicy, expand_vars};
14
15pub const PLUGIN_ROOT_VARS: &[&str] = &["CALIBAN_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
17
18fn plugin_ctx(plugin_root: &Path) -> ExpandContext {
19 let root = plugin_root.to_string_lossy().into_owned();
20 let mut ctx = ExpandContext {
21 allow_default: false,
24 missing_policy: MissingPolicy::PassThrough,
25 ..Default::default()
26 };
27 for v in PLUGIN_ROOT_VARS {
28 ctx.set(*v, root.clone());
29 }
30 ctx
31}
32
33#[must_use]
37pub fn expand(s: &str, plugin_root: &Path) -> String {
38 let ctx = plugin_ctx(plugin_root);
39 expand_vars(s, &ctx).unwrap_or_else(|_| {
42 let mut out = String::with_capacity(s.len());
45 if let Some(idx) = s.find("${") {
46 out.push_str(&s[..idx]);
47 out.push_str(&s[idx..]);
48 } else {
49 out.push_str(s);
50 }
51 out
52 })
53}
54
55pub fn expand_json_in_place(v: &mut serde_json::Value, plugin_root: &Path) {
59 match v {
60 serde_json::Value::String(s) => {
61 let new = expand(s, plugin_root);
62 *s = new;
63 }
64 serde_json::Value::Array(arr) => {
65 for child in arr.iter_mut() {
66 expand_json_in_place(child, plugin_root);
67 }
68 }
69 serde_json::Value::Object(map) => {
70 for (_, val) in map.iter_mut() {
71 expand_json_in_place(val, plugin_root);
72 }
73 }
74 _ => {}
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use std::path::Path;
82
83 #[test]
84 fn expands_caliban_plugin_root() {
85 let s = "${CALIBAN_PLUGIN_ROOT}/bin/x";
86 let out = expand(s, Path::new("/p/demo"));
87 assert_eq!(out, "/p/demo/bin/x");
88 }
89
90 #[test]
91 fn expands_claude_plugin_root_alias() {
92 let s = "${CLAUDE_PLUGIN_ROOT}/bin/x";
93 let out = expand(s, Path::new("/p/demo"));
94 assert_eq!(out, "/p/demo/bin/x");
95 }
96
97 #[test]
98 fn passes_through_unrelated_vars() {
99 let s = "${HOME}/keys/${CALIBAN_PLUGIN_ROOT}/bin";
100 let out = expand(s, Path::new("/p/demo"));
101 assert_eq!(out, "${HOME}/keys//p/demo/bin");
102 }
103
104 #[test]
105 fn no_braces_returns_input() {
106 let s = "no vars here";
107 assert_eq!(expand(s, Path::new("/p/demo")), s);
108 }
109
110 #[test]
111 fn unclosed_brace_passes_through() {
112 let s = "broken ${UNCLOSED";
113 let out = expand(s, Path::new("/p/demo"));
114 assert_eq!(out, "broken ${UNCLOSED");
115 }
116
117 #[test]
118 fn expands_nested_json_strings() {
119 let mut v: serde_json::Value = serde_json::json!({
120 "command": "${CALIBAN_PLUGIN_ROOT}/bin/srv",
121 "args": ["--root", "${CLAUDE_PLUGIN_ROOT}"],
122 "nested": { "path": "${CALIBAN_PLUGIN_ROOT}/sub" }
123 });
124 expand_json_in_place(&mut v, Path::new("/p/demo"));
125 assert_eq!(v["command"], "/p/demo/bin/srv");
126 assert_eq!(v["args"][1], "/p/demo");
127 assert_eq!(v["nested"]["path"], "/p/demo/sub");
128 }
129}