Skip to main content

canic_host/icp/
command.rs

1use std::{
2    path::{Path, PathBuf},
3    process::Command,
4};
5
6use super::{
7    error::IcpCommandError,
8    model::{IcpCli, LOCAL_NETWORK, LocalReplicaTarget},
9    version::compatible_version_output,
10};
11
12impl IcpCli {
13    /// Build an ICP CLI command context from an executable path and optional target.
14    #[must_use]
15    pub fn new(
16        executable: impl Into<String>,
17        environment: Option<String>,
18        network: Option<String>,
19    ) -> Self {
20        Self {
21            executable: executable.into(),
22            environment,
23            network,
24            cwd: None,
25            local_replica: None,
26        }
27    }
28
29    /// Return a copy of this ICP CLI context rooted at one project directory.
30    #[must_use]
31    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
32        self.cwd = Some(cwd.into());
33        self
34    }
35
36    /// Return a copy using an explicit direct local replica target.
37    #[must_use]
38    pub fn with_local_replica(mut self, target: Option<LocalReplicaTarget>) -> Self {
39        self.local_replica = target;
40        self
41    }
42
43    /// Return the optional ICP environment name carried by this command context.
44    #[must_use]
45    pub fn environment(&self) -> Option<&str> {
46        self.environment.as_deref()
47    }
48
49    /// Return the optional direct network name carried by this command context.
50    #[must_use]
51    pub fn network(&self) -> Option<&str> {
52        self.network.as_deref()
53    }
54
55    /// Build a base ICP CLI command from this context.
56    #[must_use]
57    pub fn command(&self) -> Command {
58        let mut command = Command::new(&self.executable);
59        if let Some(cwd) = &self.cwd {
60            command.current_dir(cwd);
61            add_project_root_override_arg(&mut command, cwd);
62        }
63        command
64    }
65
66    /// Build a base ICP CLI command rooted at one workspace directory.
67    #[must_use]
68    pub fn command_in(&self, cwd: &Path) -> Command {
69        let mut command = Command::new(&self.executable);
70        command.current_dir(cwd);
71        add_project_root_override_arg(&mut command, cwd);
72        command
73    }
74
75    /// Build an `icp canister ...` command with optional environment args applied.
76    #[must_use]
77    pub fn canister_command(&self) -> Command {
78        let mut command = self.command();
79        command.arg("canister");
80        command
81    }
82
83    pub(super) fn add_target_args(&self, command: &mut Command) {
84        add_target_args(
85            command,
86            self.environment(),
87            self.network(),
88            self.local_replica.as_ref(),
89        );
90    }
91
92    pub(super) fn add_local_network_target(&self, command: &mut Command) {
93        if let Some(environment) = self.environment() {
94            command.args(["-e", environment]);
95        } else if let Some(network) = self.network() {
96            command.arg(network);
97        } else {
98            command.arg(LOCAL_NETWORK);
99        }
100    }
101}
102
103/// Build a base `icp` command with the default executable.
104#[must_use]
105pub fn default_command() -> Command {
106    IcpCli::new("icp", None, None).command()
107}
108
109/// Build a base `icp` command rooted at one workspace directory.
110#[must_use]
111pub fn default_command_in(cwd: &Path) -> Command {
112    IcpCli::new("icp", None, None).command_in(cwd)
113}
114
115/// Add optional ICP CLI target arguments, preferring named environments.
116pub fn add_target_args(
117    command: &mut Command,
118    environment: Option<&str>,
119    network: Option<&str>,
120    local_replica: Option<&LocalReplicaTarget>,
121) {
122    if let Some(environment) = environment {
123        if environment == LOCAL_NETWORK
124            && let Some(local_replica) = local_replica
125        {
126            command.env_remove("ICP_ENVIRONMENT");
127            command
128                .arg("-n")
129                .arg(&local_replica.url)
130                .arg("-k")
131                .arg(&local_replica.root_key);
132            return;
133        }
134        command.args(["-e", environment]);
135    } else if let Some(network) = network {
136        command.args(["-n", network]);
137    }
138}
139
140/// Add ICP CLI output formatting, handling JSON as its own flag.
141pub fn add_output_arg(command: &mut Command, output: &str) {
142    if output == "json" {
143        command.arg("--json");
144    } else {
145        command.args(["--output", output]);
146    }
147}
148
149/// Add an ICP CLI local Candid interface path when one is available.
150pub fn add_candid_arg(command: &mut Command, candid_path: Option<&Path>) {
151    if let Some(candid_path) = candid_path {
152        command.arg("--candid").arg(candid_path);
153    }
154}
155
156/// Return Canic's local ICP CLI Candid sidecar path for one role.
157#[must_use]
158pub fn local_canister_candid_path(icp_root: &Path, environment: &str, role: &str) -> PathBuf {
159    icp_root
160        .join(".icp")
161        .join(environment)
162        .join("canisters")
163        .join(role)
164        .join(format!("{role}.did"))
165}
166
167/// Return the local Candid sidecar path only when it exists on disk.
168#[must_use]
169pub fn existing_local_canister_candid_path(
170    icp_root: &Path,
171    environment: &str,
172    role: &str,
173) -> Option<PathBuf> {
174    let path = local_canister_candid_path(icp_root, environment, role);
175    path.is_file().then_some(path)
176}
177
178/// Add ICP CLI debug logging when requested.
179pub fn add_debug_arg(command: &mut Command, debug: bool) {
180    if debug {
181        command.arg("--debug");
182    }
183}
184
185/// Ensure a command points at a supported ICP CLI executable before spawning it.
186pub fn ensure_command_compatible(command: &Command) -> Result<(), IcpCommandError> {
187    let executable = command.get_program().to_string_lossy();
188    compatible_version_output(executable.as_ref(), command.get_current_dir()).map(|_| ())
189}
190
191fn add_project_root_override_arg(command: &mut Command, cwd: &Path) {
192    command.arg("--project-root-override").arg(cwd);
193}
194
195/// Render a command for diagnostics and dry-run previews.
196#[must_use]
197pub fn command_display(command: &Command) -> String {
198    let mut parts = vec![command.get_program().to_string_lossy().to_string()];
199    parts.extend(
200        command
201            .get_args()
202            .map(|arg| arg.to_string_lossy().to_string()),
203    );
204    parts.join(" ")
205}