Skip to main content

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>;