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 SnapshotIdUnavailable {
32 output: String,
33 },
34}
35
36impl IcpCommandError {
37 #[must_use]
39 pub fn diagnostic(&self) -> Option<IcpDiagnostic> {
40 self.external_output().and_then(classify_icp_diagnostic)
41 }
42
43 #[must_use]
45 pub fn external_output(&self) -> Option<&str> {
46 match self {
47 Self::Failed { stderr, .. } => Some(stderr),
48 Self::Json { output, .. } | Self::SnapshotIdUnavailable { output } => Some(output),
49 Self::Io(_) | Self::MissingCli { .. } | Self::IncompatibleCliVersion { .. } => None,
50 }
51 }
52}
53
54impl fmt::Display for IcpCommandError {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::Io(err) => write!(formatter, "{err}"),
59 Self::MissingCli { executable } => {
60 write!(
61 formatter,
62 "icp-cli executable not found: {executable}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
63 icp_cli_install_hint(),
64 )
65 }
66 Self::IncompatibleCliVersion { executable, found } => {
67 write!(
68 formatter,
69 "unsupported icp-cli version for {executable}\nfound: {found}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\n{}",
70 icp_cli_install_hint(),
71 )
72 }
73 Self::Failed { command, stderr } => {
74 write!(formatter, "icp command failed: {command}\n{stderr}")
75 }
76 Self::Json {
77 command,
78 output,
79 source,
80 } => {
81 write!(
82 formatter,
83 "could not parse icp json output for {command}: {source}\n{output}"
84 )
85 }
86 Self::SnapshotIdUnavailable { output } => {
87 write!(
88 formatter,
89 "could not parse snapshot id from icp output: {output}"
90 )
91 }
92 }
93 }
94}
95
96fn icp_cli_install_hint() -> String {
97 format!(
98 "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"
99 )
100}
101
102impl Error for IcpCommandError {
103 fn source(&self) -> Option<&(dyn Error + 'static)> {
105 match self {
106 Self::Io(err) => Some(err),
107 Self::Json { source, .. } => Some(source),
108 Self::Failed { .. }
109 | Self::IncompatibleCliVersion { .. }
110 | Self::MissingCli { .. }
111 | Self::SnapshotIdUnavailable { .. } => None,
112 }
113 }
114}
115
116impl From<std::io::Error> for IcpCommandError {
117 fn from(err: std::io::Error) -> Self {
119 Self::Io(err)
120 }
121}