mod manifest;
use std::{
io,
process::{Command, Output, Stdio},
};
use ic_host_text::escape::terminal_text;
use ic_host_tools::{icp::decode_canister_list, icp_project::environment_network};
use crate::icp::process::{PROBE_STDOUT_BYTES, canister_id, output_stderr, probe_output};
pub(super) fn require_created_canister(environment: &str, canister: &str) -> Result<(), String> {
match canister_id(environment, canister) {
Ok(Some(_)) => Ok(()),
Ok(None) => Err(missing_canister_message(environment, canister)),
Err(err) => Err(err),
}
}
pub(super) fn known_canisters(environment: &str) -> Result<Vec<String>, String> {
let fallback_reason = match icp_canister_list_output(environment) {
Ok(output) if output.status.success() => return parse_icp_canister_list(&output.stdout),
Ok(output) => format!(
"icp canister list for environment '{environment}' failed with {}: {}",
output.status,
output_stderr(&output.stderr),
environment = terminal_text(environment),
),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
format!(
"run icp canister list for environment '{}': {error}",
terminal_text(environment)
)
}
Err(error) => {
return Err(format!(
"run icp canister list for environment '{}': {error}",
terminal_text(environment),
));
}
};
let canisters = manifest::known_canisters(environment)
.map_err(|error| format!("{fallback_reason}\n{error}"))?;
eprintln!(
"[icydb] {}; using canister names from icp.yaml",
output_stderr(fallback_reason.as_bytes()),
);
Ok(canisters)
}
pub(super) fn environment_targets_local(environment: &str) -> bool {
let Ok(output) = probe_output(
Command::new("icp")
.args(["project", "show"])
.stdin(Stdio::null()),
) else {
return false;
};
output.status.success()
&& environment_network(&output.stdout, environment, PROBE_STDOUT_BYTES)
.is_ok_and(|network| network.as_deref() == Some("local"))
}
fn icp_canister_list_output(environment: &str) -> io::Result<Output> {
probe_output(
Command::new("icp")
.arg("canister")
.arg("list")
.arg("--json")
.arg("--environment")
.arg(environment),
)
}
fn parse_icp_canister_list(output: &[u8]) -> Result<Vec<String>, String> {
let canisters = decode_canister_list(output, PROBE_STDOUT_BYTES).map_err(|error| {
format!(
"parse icp canister list --json: {}",
terminal_text(&error.to_string())
)
})?;
Ok(sorted_canister_names(canisters.into_iter()))
}
fn sorted_canister_names(names: impl Iterator<Item = String>) -> Vec<String> {
let mut names = names.collect::<Vec<_>>();
names.sort();
names
}
fn missing_canister_message(environment: &str, canister: &str) -> String {
let mut message = missing_canister_base_message(environment, canister);
match known_canisters(environment) {
Ok(canisters) if !canisters.is_empty() => {
append_known_canisters(&mut message, canisters.as_slice());
}
Err(error) => {
message.push_str("\nCould not read known canisters: ");
message.push_str(&error);
}
Ok(_) => {}
}
message
}
fn missing_canister_base_message(environment: &str, canister: &str) -> String {
format!(
"canister '{canister}' is not created in the '{environment}' ICP environment.\nVerify that the environment was selected intentionally, then run `icydb canister list --environment {environment}` to see its known canisters.\nDo not refresh unless this explicitly selected environment is disposable and you intend to rebuild its canister state.\nThe CLI never starts or stops the ICP network; manage that lifecycle outside icydb.",
canister = terminal_text(canister),
environment = terminal_text(environment),
)
}
fn append_known_canisters(message: &mut String, canisters: &[String]) {
message.push_str("\nKnown canisters from icp-cli: ");
message.push_str(&terminal_text(&canisters.join(", ")));
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod helper_tests;
#[cfg(test)]
pub(super) use helper_tests::parse_manifest_canisters;