1pub mod builtin;
2pub mod custom;
3pub mod path_guard;
4pub mod hook_config;
5pub use builtin::ClaudeWrapper;
6pub use custom::{WrapperKind, Manifest};
7pub use path_guard::PathGuard;
8
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12pub const CONTRACT_VERSION: u32 = 1;
13
14pub struct WrapperContext {
15 pub worker_name: String,
16 pub ticket_id: String,
17 pub ticket_branch: String,
18 pub worktree_path: PathBuf,
19 pub system_prompt_file: PathBuf,
20 pub user_message_file: PathBuf,
21 pub skip_permissions: bool,
22 pub profile: String,
23 pub role_prefix: Option<String>,
24 pub options: HashMap<String, String>,
25 pub model: Option<String>,
26 pub log_path: PathBuf,
27 pub container: Option<String>,
28 pub extra_env: HashMap<String, String>,
29 pub root: PathBuf,
30 pub keychain: HashMap<String, String>,
31 pub current_state: String,
32 pub command: Option<String>,
36}
37
38pub trait Wrapper {
39 fn spawn(&self, ctx: &WrapperContext) -> anyhow::Result<std::process::Child>;
40}
41
42pub fn resolve_builtin(name: &str) -> Option<Box<dyn Wrapper>> {
43 match name {
44 "claude" => Some(Box::new(builtin::ClaudeWrapper)),
45 "mock-happy" => Some(Box::new(builtin::MockHappyWrapper)),
46 "mock-sad" => Some(Box::new(builtin::MockSadWrapper)),
47 "mock-random" => Some(Box::new(builtin::MockRandomWrapper)),
48 "debug" => Some(Box::new(builtin::DebugWrapper)),
49 _ => None,
50 }
51}
52
53pub fn list_builtin_names() -> &'static [&'static str] {
54 &["claude", "mock-happy", "mock-sad", "mock-random", "debug"]
55}
56
57pub fn resolve_wrapper(root: &Path, name: &str) -> anyhow::Result<Option<WrapperKind>> {
58 if let Some(script_path) = custom::find_script(root, name) {
59 let manifest = custom::parse_manifest(root, name)?;
60 return Ok(Some(WrapperKind::Custom { script_path, manifest }));
61 }
62 if resolve_builtin(name).is_some() {
63 return Ok(Some(WrapperKind::Builtin(name.to_owned())));
64 }
65 Ok(None)
66}
67
68pub fn write_temp_file(prefix: &str, content: &str) -> anyhow::Result<PathBuf> {
69 let path = std::env::temp_dir().join(format!("apm-{prefix}-{:04x}.txt", rand_u16()));
70 std::fs::write(&path, content)?;
71 Ok(path)
72}
73
74pub(crate) fn rand_u16() -> u16 {
75 use std::time::{SystemTime, UNIX_EPOCH};
76 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().subsec_nanos() as u16
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn resolve_builtin_claude_returns_some() {
85 assert!(resolve_builtin("claude").is_some());
86 }
87
88 #[test]
89 fn resolve_builtin_unknown_returns_none() {
90 assert!(resolve_builtin("bogus").is_none());
91 assert!(resolve_builtin("").is_none());
92 }
93
94 #[test]
95 fn resolve_builtin_mock_happy_returns_some() {
96 assert!(resolve_builtin("mock-happy").is_some());
97 }
98
99 #[test]
100 fn resolve_builtin_mock_sad_returns_some() {
101 assert!(resolve_builtin("mock-sad").is_some());
102 }
103
104 #[test]
105 fn resolve_builtin_mock_random_returns_some() {
106 assert!(resolve_builtin("mock-random").is_some());
107 }
108
109 #[test]
110 fn resolve_builtin_debug_returns_some() {
111 assert!(resolve_builtin("debug").is_some());
112 }
113}