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