1use std::{error::Error, fmt};
2
3use super::{
4 diagnostic::{IcpDiagnostic, classify_icp_diagnostic},
5 model::{ICP_CLI_SUPPORTED_VERSION_RANGE, REQUIRED_ICP_CLI_VERSION},
6};
7
8#[derive(Debug)]
13pub enum IcpCommandError {
14 Io(std::io::Error),
15 MissingCli {
16 executable: String,
17 },
18 IncompatibleCliVersion {
19 executable: String,
20 found: String,
21 },
22 Failed {
23 command: String,
24 stderr: String,
25 },
26 Json {
27 command: String,
28 output: String,
29 source: serde_json::Error,
30 },
31}
32
33impl IcpCommandError {
34 #[must_use]
36 pub fn diagnostic(&self) -> Option<IcpDiagnostic> {
37 self.external_output().and_then(classify_icp_diagnostic)
38 }
39
40 #[must_use]
42 pub fn external_output(&self) -> Option<&str> {
43 match self {
44 Self::Failed { stderr, .. } => Some(stderr),
45 Self::Json { output, .. } => Some(output),
46 Self::Io(_) | Self::MissingCli { .. } | Self::IncompatibleCliVersion { .. } => None,
47 }
48 }
49}
50
51impl fmt::Display for IcpCommandError {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::Io(err) => write!(formatter, "{err}"),
56 Self::MissingCli { executable } => {
57 write!(
58 formatter,
59 "icp-cli executable not found: {executable}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
60 icp_cli_install_hint(),
61 )
62 }
63 Self::IncompatibleCliVersion { executable, found } => {
64 write!(
65 formatter,
66 "unsupported icp-cli version for {executable}\nfound: {found}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
67 icp_cli_install_hint(),
68 )
69 }
70 Self::Failed { command, stderr } => {
71 write!(formatter, "icp command failed: {command}\n{stderr}")
72 }
73 Self::Json {
74 command,
75 output,
76 source,
77 } => {
78 write!(
79 formatter,
80 "could not parse icp json output for {command}: {source}\n{output}"
81 )
82 }
83 }
84 }
85}
86
87fn icp_cli_install_hint() -> String {
88 format!(
89 "next: install checksum-verified icp-cli {REQUIRED_ICP_CLI_VERSION} or newer and ensure it is first on PATH\n from a Canic checkout: make install-dev\nnote: `icp network update` updates the local network launcher, not the `icp` CLI binary; pass top-level --icp <path> to use a non-PATH install"
90 )
91}
92
93impl Error for IcpCommandError {
94 fn source(&self) -> Option<&(dyn Error + 'static)> {
96 match self {
97 Self::Io(err) => Some(err),
98 Self::Json { source, .. } => Some(source),
99 Self::Failed { .. } | Self::IncompatibleCliVersion { .. } | Self::MissingCli { .. } => {
100 None
101 }
102 }
103 }
104}
105
106impl From<std::io::Error> for IcpCommandError {
107 fn from(err: std::io::Error) -> Self {
109 Self::Io(err)
110 }
111}