Skip to main content

safe_chains/targets/
opencode.rs

1use std::path::{Path, PathBuf};
2
3use super::{InstallOutcome, Target};
4
5pub struct OpenCodeTarget;
6
7impl Target for OpenCodeTarget {
8    fn name(&self) -> &'static str {
9        "opencode"
10    }
11
12    fn display_name(&self) -> &'static str {
13        "OpenCode"
14    }
15
16    fn detect_paths(&self, home: &Path) -> Vec<PathBuf> {
17        vec![home.join(".config").join("opencode")]
18    }
19
20    fn install(&self, _home: &Path) -> Result<InstallOutcome, String> {
21        // opencode has no integration point safe-chains can use YET:
22        //  - No runtime hook — its `permission.ask` plugin hook is defined in the SDK but never fires
23        //    (github.com/anomalyco/opencode/issues/7006).
24        //  - The only alternative, a static `permission.bash` glob allowlist, cannot represent
25        //    safe-chains' per-ARGUMENT classification (`"git *"` would allow `git push` as well as
26        //    `git status`). A previous `--opencode-config` generator was inert (empty pattern set) and
27        //    was DROPPED as misleading. Revisit when #7006 lands (a real runtime hook).
28        Ok(InstallOutcome::Skipped {
29            reason: "OpenCode has no runtime hook safe-chains can drive yet (its plugin hook does not \
30                     fire — opencode #7006), and a static permission glob can't represent per-argument \
31                     safety. Not integrated — watching for an upstream hook."
32                .to_string(),
33        })
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn install_returns_skip_explaining_no_integration() {
43        let dir = tempfile::tempdir().unwrap();
44        let outcome = OpenCodeTarget.install(dir.path()).unwrap();
45        match outcome {
46            InstallOutcome::Skipped { reason } => {
47                assert!(reason.to_lowercase().contains("hook"));
48            }
49            other => panic!("expected Skipped, got {:?}", std::mem::discriminant(&other)),
50        }
51    }
52}