1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
//! Error types for the skills CLI.
use std::{
env::VarError,
io,
path::PathBuf,
process::{ExitCode, ExitStatus},
result::Result as StdResult,
};
use thiserror::Error;
use toml::{de::Error as TomlError, ser::Error as TomlSerError};
/// Result type for skills operations.
pub type Result<T> = StdResult<T, Error>;
/// Errors that can occur while running the CLI.
#[derive(Debug, Error)]
pub enum Error {
/// No configured sources were found in the config file.
#[error("No sources configured; edit {config_path} to add at least one source.")]
NoSources {
/// Path to the config file.
config_path: PathBuf,
},
/// The configuration file could not be read.
#[error("Failed to read config at {path}: {source}")]
ConfigRead {
/// Path to the config file.
path: PathBuf,
/// Underlying IO error.
source: io::Error,
},
/// The configuration file could not be parsed.
#[error("Failed to parse config at {path}: {source}")]
ConfigParse {
/// Path to the config file.
path: PathBuf,
/// Underlying parse error.
source: TomlError,
},
/// The configuration file could not be serialized.
#[error("Failed to serialize config: {source}")]
ConfigSerialize {
/// Underlying serialization error.
source: TomlSerError,
},
/// The configuration file could not be written.
#[error("Failed to write config at {path}: {source}")]
ConfigWrite {
/// Path to the config file.
path: PathBuf,
/// Underlying IO error.
source: io::Error,
},
/// Home directory resolution failed.
#[error("Failed to resolve the home directory.")]
HomeDirMissing,
/// A configured path could not be expanded.
#[error("Invalid path in config: {path}: {source}")]
PathExpansion {
/// Input path that failed to expand.
path: String,
/// Underlying expansion error.
source: shellexpand::LookupError<VarError>,
},
/// A configured path was not valid Unicode for expansion.
#[error("Invalid path in config: {path}")]
PathNotUnicode {
/// Path that could not be represented as UTF-8.
path: PathBuf,
},
/// A tool pager was specified but could not be parsed.
#[error("Invalid pager command: {message}")]
PagerParse {
/// Error message describing the parse failure.
message: String,
},
/// A pager command could not be spawned.
#[error("Failed to run pager `{pager}`: {source}")]
PagerSpawn {
/// Pager command that failed to spawn.
pager: String,
/// Underlying spawn error.
source: io::Error,
},
/// A pager process exited with a non-zero status.
#[error("Pager `{pager}` exited with status {status}")]
PagerStatus {
/// Pager command that exited.
pager: String,
/// Exit status returned by the pager.
status: ExitStatus,
},
/// Failed to write to a pager process.
#[error("Failed to write to pager `{pager}`: {source}")]
PagerWrite {
/// Pager command that failed to receive input.
pager: String,
/// Underlying write error.
source: io::Error,
},
/// An interactive prompt was interrupted or canceled.
#[error("Prompt canceled.")]
PromptCanceled,
/// An interactive prompt failed.
#[error("Prompt failed: {message}")]
PromptFailed {
/// Error message describing the prompt failure.
message: String,
},
/// A skill could not be found.
#[error("Skill not found: {name}")]
SkillNotFound {
/// Missing skill name.
name: String,
},
/// A required path already exists.
#[error("Path already exists: {path}")]
PathExists {
/// Path that already exists.
path: PathBuf,
},
/// A path was not a valid file system location.
#[error("Invalid path: {path}")]
InvalidPath {
/// Path that could not be used.
path: PathBuf,
},
/// A path required for operation does not exist.
#[error("Path does not exist: {path}")]
PathMissing {
/// Path that does not exist.
path: PathBuf,
},
/// A skill file could not be written.
#[error("Failed to write skill file at {path}: {source}")]
SkillWrite {
/// Path that failed to write.
path: PathBuf,
/// Underlying IO error.
source: io::Error,
},
/// A skill file could not be read.
#[error("Failed to read skill file at {path}: {source}")]
SkillRead {
/// Path that failed to read.
path: PathBuf,
/// Underlying IO error.
source: io::Error,
},
/// A template could not be rendered.
#[error("Failed to render template: {message}")]
TemplateRender {
/// Error message describing the render failure.
message: String,
},
/// No local skill was found with the given name.
#[error("No local skill named '{name}' found in .claude/skills/ or .codex/skills/")]
LocalSkillNotFound {
/// Missing skill name.
name: String,
},
/// A skill already exists at the target location.
#[error("Skill '{name}' already exists at {path}. Use --force to overwrite.")]
SkillExists {
/// Skill name.
name: String,
/// Path where the skill exists.
path: PathBuf,
},
/// Failed to move a skill directory.
#[error("Failed to move skill from {from} to {to}: {source}")]
SkillMove {
/// Source path.
from: PathBuf,
/// Destination path.
to: PathBuf,
/// Underlying IO error.
source: io::Error,
},
/// Multiple local skills found with the same name.
#[error("Multiple local skills named '{name}' found. Specify --tool to disambiguate.")]
AmbiguousLocalSkill {
/// Ambiguous skill name.
name: String,
},
/// Failed to create a ZIP archive.
#[error("Failed to create ZIP archive at {path}: {message}")]
ZipCreate {
/// Path to the ZIP file.
path: PathBuf,
/// Error message.
message: String,
},
/// Failed to read a ZIP archive.
#[error("Failed to read ZIP archive at {path}: {message}")]
ZipRead {
/// Path to the ZIP file.
path: PathBuf,
/// Error message.
message: String,
},
/// Invalid ZIP structure.
#[error("Invalid ZIP file: {message}")]
InvalidZip {
/// Error message describing the invalid structure.
message: String,
},
/// Failed to download from URL.
#[error("Failed to download from {url}: {message}")]
Download {
/// URL that failed.
url: String,
/// Error message.
message: String,
},
/// Invalid URL.
#[error("Invalid URL: {url}")]
InvalidUrl {
/// The invalid URL.
url: String,
},
/// HTTP URL not allowed.
#[error("HTTP URLs not allowed for security. Use HTTPS: {url}")]
HttpNotAllowed {
/// The HTTP URL.
url: String,
},
/// File too large.
#[error("File too large: {size} bytes (max {max} bytes)")]
FileTooLarge {
/// Actual size.
size: u64,
/// Maximum allowed size.
max: u64,
},
/// Sync conflict between tools.
#[error("Conflict: skill '{name}' has divergent modifications in {tools}. Resolve manually.")]
SyncConflict {
/// Skill name with conflict.
name: String,
/// Tools with conflicting changes.
tools: String,
},
/// Failed to launch the editor.
#[error("Failed to run editor `{editor}`: {message}")]
EditorFailed {
/// Editor command that failed.
editor: String,
/// Error message.
message: String,
},
/// Validation failed.
#[error("Validation failed: {message}")]
ValidationFailed {
/// Error message describing the validation failure.
message: String,
},
}
impl Error {
/// Map errors to exit codes for CLI termination.
pub fn exit_code(&self) -> ExitCode {
ExitCode::from(1)
}
}