skill_embed/error.rs
1use std::fmt;
2use std::io;
3use std::path::PathBuf;
4
5use crate::InstallStatus;
6
7/// Everything this library refuses to do, and why.
8///
9/// Each variant a user's own input can cause is one a front end can match on,
10/// so a mistyped flag is told apart from a disk that is full:
11///
12/// ```no_run
13/// # let skills: skill_embed::Installer = unimplemented!();
14/// # let options = skill_embed::InstallOptions::default();
15/// let (results, outcome) = skills.install(&options);
16/// print!("{}", skill_embed::render_results(&results, options.dry_run));
17/// if let Err(skill_embed::Error::NeedsForce(_)) = outcome {
18/// // tell the user to re-run with --force
19/// }
20/// ```
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum Error {
24 /// `--agent` named no agent this tool offers.
25 #[error("unknown agent {name:?} (want one of {}, or all, or detected)", valid.join(", "))]
26 UnknownAgent {
27 /// The name that was given.
28 name: String,
29 /// The names that would have been accepted.
30 valid: Vec<String>,
31 },
32
33 /// `--scope` was neither `project` nor `user`.
34 #[error("unknown scope {value:?} (want project or user)")]
35 UnknownScope {
36 /// The value that was given.
37 value: String,
38 },
39
40 /// A named skill is not embedded.
41 #[error("unknown skill {name:?} (embedded: {})", embedded.join(", "))]
42 UnknownSkill {
43 /// The name that was given.
44 name: String,
45 /// The names the binary carries.
46 embedded: Vec<String>,
47 },
48
49 /// The `--agent` values resolved to nothing at all.
50 #[error("no agent selected")]
51 NoAgentSelected,
52
53 /// A destination held something this tool did not write, or something
54 /// edited after it did.
55 ///
56 /// Install leaves those skills alone and writes the rest, so the results it
57 /// returns alongside this are still worth reporting.
58 #[error(transparent)]
59 NeedsForce(#[from] ForceRequired),
60
61 /// Project scope resolved to the home directory.
62 ///
63 /// The user scope directories live there. A project installation written
64 /// into them would put one project's skills in front of every other
65 /// project, and `--scope user` already writes there on purpose.
66 #[error(
67 "the search landed on the home directory ({}), so pass --scope user to write there \
68 on purpose or --dir to name another directory",
69 dir.display()
70 )]
71 ProjectIsHome {
72 /// The directory the search landed on.
73 dir: PathBuf,
74 },
75
76 /// A project scope destination is outside the project root.
77 ///
78 /// The path a project install writes to comes from the project, so a link
79 /// committed at `.claude/skills`, or at any directory above it, aims the
80 /// write and a later forced removal wherever it points. Cloning a
81 /// repository and running the tool once is the whole of it. User scope and
82 /// `--dir` are the user naming a place, and are not bounded this way.
83 #[error(
84 "the destination is outside the project root ({} is really {}), so pass --dir to \
85 write there on purpose",
86 dir.display(),
87 real.display()
88 )]
89 ProjectEscapes {
90 /// The destination as it was resolved.
91 dir: PathBuf,
92 /// Where it really lands.
93 real: PathBuf,
94 },
95
96 /// The command line could not be read.
97 #[error("{0}")]
98 Usage(String),
99
100 /// The embedded skills could not be read as skills. It is a build-time
101 /// mistake rather than a runtime condition.
102 #[error("{0}")]
103 Skills(String),
104
105 /// The run was cancelled through [`InstallOptions::cancel`].
106 ///
107 /// [`InstallOptions::cancel`]: crate::InstallOptions::cancel
108 #[error("cancelled")]
109 Cancelled,
110
111 /// Help was asked for and printed. It is not a failure.
112 #[error("help requested")]
113 Help,
114
115 /// Something the file system refused.
116 #[error("{context}: {source}")]
117 Io {
118 /// What was being done.
119 context: String,
120 /// What the file system said.
121 #[source]
122 source: io::Error,
123 },
124}
125
126impl Error {
127 pub(crate) fn io(context: impl Into<String>, source: io::Error) -> Self {
128 Self::Io { context: context.into(), source }
129 }
130}
131
132/// Every destination install left alone, and why the list is worth printing.
133///
134/// Its message runs to several lines, one per destination, which is why it is a
135/// type of its own rather than a variant with a one line format string.
136#[derive(Debug)]
137pub struct ForceRequired {
138 /// The state of each destination that was not written.
139 pub blocked: Vec<InstallStatus>,
140}
141
142impl fmt::Display for ForceRequired {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(
145 "refusing to overwrite skills this tool did not install, \
146 or that were edited after installing:\n",
147 )?;
148 for st in &self.blocked {
149 writeln!(f, " {} ({})", st.path.display(), st.state)?;
150 }
151 f.write_str("re-run with --force to overwrite")
152 }
153}
154
155impl std::error::Error for ForceRequired {}
156
157/// The result of anything in this crate that can be refused.
158pub type Result<T, E = Error> = std::result::Result<T, E>;