1use std::process;
7
8pub mod exit_code {
10 pub const ERROR: i32 = 1;
12 pub const USER_CANCELLED: i32 = 2;
14}
15
16#[derive(Debug, thiserror::Error)]
18pub enum PawError {
19 #[error("Not a git repository. Run git-paw from inside a git project.")]
21 NotAGitRepo,
22
23 #[error(
25 "tmux is required but not installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
26 )]
27 TmuxNotInstalled,
28
29 #[error(
31 "No AI CLIs found on PATH. Install one or use `git paw add-cli` to register a custom CLI."
32 )]
33 NoCLIsFound,
34
35 #[error("Worktree error: {0}")]
37 WorktreeError(String),
38
39 #[error("Session error: {0}")]
41 SessionError(String),
42
43 #[error("Config error: {0}")]
45 ConfigError(String),
46
47 #[error("Branch error: {0}")]
49 BranchError(String),
50
51 #[error("Cancelled.")]
53 UserCancelled,
54
55 #[error("Tmux error: {0}")]
57 TmuxError(String),
58
59 #[error("CLI '{0}' not found in config")]
61 CliNotFound(String),
62
63 #[error("Init error: {0}")]
65 InitError(String),
66
67 #[error("AGENTS.md error: {0}")]
69 AgentsMdError(String),
70
71 #[error("Spec error: {0}")]
73 SpecError(String),
74
75 #[error("Replay error: {0}")]
77 ReplayError(String),
78}
79
80impl PawError {
81 pub fn exit_code(&self) -> i32 {
83 match self {
84 Self::UserCancelled => exit_code::USER_CANCELLED,
85 _ => exit_code::ERROR,
86 }
87 }
88
89 pub fn exit(&self) -> ! {
91 eprintln!("error: {self}");
92 process::exit(self.exit_code());
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn test_not_a_git_repo_is_actionable() {
102 let msg = PawError::NotAGitRepo.to_string();
103 assert!(msg.contains("git repository"), "should explain the problem");
104 assert!(msg.contains("git-paw"), "should name the tool");
105 }
106
107 #[test]
108 fn test_tmux_not_installed_includes_install_instructions() {
109 let msg = PawError::TmuxNotInstalled.to_string();
110 assert!(msg.contains("tmux"), "should name the missing dependency");
111 assert!(
112 msg.contains("brew install"),
113 "should include macOS install hint"
114 );
115 assert!(
116 msg.contains("apt install"),
117 "should include Linux install hint"
118 );
119 }
120
121 #[test]
122 fn test_no_clis_found_suggests_add_cli() {
123 let msg = PawError::NoCLIsFound.to_string();
124 assert!(
125 msg.contains("add-cli"),
126 "should suggest the add-cli command"
127 );
128 }
129
130 #[test]
131 fn test_worktree_error_includes_detail() {
132 let msg = PawError::WorktreeError("failed to create".into()).to_string();
133 assert!(
134 msg.contains("failed to create"),
135 "should include the inner detail"
136 );
137 }
138
139 #[test]
140 fn test_session_error_includes_detail() {
141 let msg = PawError::SessionError("file corrupt".into()).to_string();
142 assert!(
143 msg.contains("file corrupt"),
144 "should include the inner detail"
145 );
146 }
147
148 #[test]
149 fn test_config_error_includes_detail() {
150 let msg = PawError::ConfigError("invalid toml".into()).to_string();
151 assert!(
152 msg.contains("invalid toml"),
153 "should include the inner detail"
154 );
155 }
156
157 #[test]
158 fn test_branch_error_includes_detail() {
159 let msg = PawError::BranchError("not found".into()).to_string();
160 assert!(msg.contains("not found"), "should include the inner detail");
161 }
162
163 #[test]
164 fn test_user_cancelled_is_not_empty() {
165 let msg = PawError::UserCancelled.to_string();
166 assert!(!msg.is_empty(), "should have a message");
167 }
168
169 #[test]
170 fn test_tmux_error_includes_detail() {
171 let msg = PawError::TmuxError("session failed".into()).to_string();
172 assert!(
173 msg.contains("session failed"),
174 "should include the inner detail"
175 );
176 }
177
178 #[test]
179 fn test_cli_not_found_includes_cli_name() {
180 let msg = PawError::CliNotFound("my-agent".into()).to_string();
181 assert!(
182 msg.contains("my-agent"),
183 "should include the missing CLI name"
184 );
185 }
186
187 #[test]
188 fn test_user_cancelled_exit_code() {
189 assert_eq!(
190 PawError::UserCancelled.exit_code(),
191 exit_code::USER_CANCELLED
192 );
193 }
194
195 #[test]
196 fn test_general_errors_exit_code() {
197 let errors: Vec<PawError> = vec![
198 PawError::NotAGitRepo,
199 PawError::TmuxNotInstalled,
200 PawError::NoCLIsFound,
201 PawError::WorktreeError("test".into()),
202 PawError::SessionError("test".into()),
203 PawError::ConfigError("test".into()),
204 PawError::BranchError("test".into()),
205 PawError::TmuxError("test".into()),
206 PawError::CliNotFound("test".into()),
207 ];
208 for err in errors {
209 assert_eq!(err.exit_code(), exit_code::ERROR, "failed for {err:?}");
210 }
211 }
212
213 #[test]
214 fn test_spec_error_includes_detail() {
215 let msg = PawError::SpecError("bad format".into()).to_string();
216 assert!(
217 msg.contains("bad format"),
218 "should include the inner detail"
219 );
220 assert!(
221 msg.contains("Spec error"),
222 "should have the Spec error prefix"
223 );
224 }
225
226 #[test]
227 fn test_spec_error_exit_code() {
228 assert_eq!(
229 PawError::SpecError("test".into()).exit_code(),
230 exit_code::ERROR
231 );
232 }
233
234 #[test]
235 fn test_agents_md_error_includes_detail() {
236 let msg = PawError::AgentsMdError("cannot write file".into()).to_string();
237 assert!(
238 msg.contains("AGENTS.md error"),
239 "should include AGENTS.md prefix"
240 );
241 assert!(
242 msg.contains("cannot write file"),
243 "should include the inner detail"
244 );
245 assert_eq!(
246 PawError::AgentsMdError("x".into()).exit_code(),
247 exit_code::ERROR,
248 "should use general exit code"
249 );
250 }
251
252 #[test]
253 fn test_debug_derived() {
254 let err = PawError::NotAGitRepo;
255 let debug = format!("{err:?}");
256 assert!(debug.contains("NotAGitRepo"));
257 }
258}