use colored::Colorize;
use error_stack::{Report, ResultExt};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde_json::{json, Value};
use std::collections::{HashSet, VecDeque};
use std::{error::Error, fmt};
use tokio::task::JoinSet;
use crate::batbelt::bat_dialoguer::BatDialoguer;
use crate::batbelt::evm::metadata::bat_metadata::{EvmBatMetadata, MiroFrameRef};
use crate::batbelt::miro::connector::create_connector_with_color;
use crate::batbelt::miro::frame::{
MiroFrame, MIRO_BOARD_COLUMNS, MIRO_FRAME_HEIGHT, MIRO_FRAME_WIDTH, MIRO_INITIAL_X,
MIRO_INITIAL_Y,
};
use crate::batbelt::miro::MiroConfig;
use crate::batbelt::parser::source_code_parser::{SourceCodeParser, SourceCodeScreenshotOptions};
use crate::config::BatConfig;
#[derive(Debug)]
pub struct EvmMiroError;
impl fmt::Display for EvmMiroError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("EvmMiro error")
}
}
impl Error for EvmMiroError {}
pub type EvmMiroResult<T> = error_stack::Result<T, EvmMiroError>;
#[derive(Clone)]
struct MiroApiConfig {
access_token: String,
board_id: String,
board_url: String,
}
struct FrameDeployResult {
entry_point_name: String,
frame_id: String,
frame_url: String,
}
pub async fn deploy_co_frames() -> EvmMiroResult<()> {
MiroConfig::check_miro_enabled().change_context(EvmMiroError)?;
let api_config = read_miro_config()?;
let evm_metadata = EvmBatMetadata::read_metadata().change_context(EvmMiroError)?;
let ep_names: Vec<String> = evm_metadata
.entry_points
.iter()
.map(|ep| ep.name.clone())
.collect();
let mut to_deploy: Vec<(String, usize)> = Vec::new();
for (idx, ep_name) in ep_names.iter().enumerate() {
if let Some(existing) = evm_metadata.get_miro_frame_by_ep_name(ep_name) {
if verify_frame_exists(&api_config, &existing.frame_id).await {
println!(
" {} already deployed: {}",
ep_name.green(),
existing.frame_url
);
continue;
}
println!(" {} stale frame, redeploying...", ep_name.bright_yellow());
}
to_deploy.push((ep_name.clone(), idx));
}
if to_deploy.is_empty() {
println!(" All {} frames already deployed", ep_names.len());
return Ok(());
}
println!(" Deploying {} frames in parallel...", to_deploy.len());
let mut join_set = JoinSet::new();
for (ep_name, idx) in to_deploy {
let config = api_config.clone();
join_set.spawn(async move {
let result = deploy_single_frame(&config, &ep_name, idx).await;
(ep_name, result)
});
}
let mut deployed_frames: Vec<FrameDeployResult> = Vec::new();
let mut errors: Vec<String> = Vec::new();
while let Some(result) = join_set.join_next().await {
match result {
Ok((ep_name, Ok((frame_id, frame_url)))) => {
println!(" {} deployed: {}", ep_name.green(), frame_url);
deployed_frames.push(FrameDeployResult {
entry_point_name: ep_name,
frame_id,
frame_url,
});
}
Ok((ep_name, Err(e))) => {
errors.push(ep_name.clone());
println!(" {} failed: {:?}", ep_name.red(), e);
}
Err(e) => {
errors.push(format!("task panic: {}", e));
}
}
}
if !deployed_frames.is_empty() {
EvmBatMetadata::update_metadata(|metadata| {
for frame in &deployed_frames {
metadata
.miro
.frames
.retain(|f| f.entry_point_name != frame.entry_point_name);
metadata.miro.frames.push(MiroFrameRef {
entry_point_name: frame.entry_point_name.clone(),
frame_id: frame.frame_id.clone(),
frame_url: frame.frame_url.clone(),
images_deployed: false,
entry_point_image_id: String::new(),
validations_image_id: String::new(),
dependency_image_ids: vec![],
});
}
})
.change_context(EvmMiroError)?;
}
if !errors.is_empty() {
println!(
" {} {} frames failed to deploy",
"Warning:".bright_yellow(),
errors.len()
);
}
Ok(())
}
fn read_miro_config() -> EvmMiroResult<MiroApiConfig> {
let bat_config = BatConfig::get_config().change_context(EvmMiroError)?;
let bat_auditor_config =
crate::config::BatAuditorConfig::get_config().change_context(EvmMiroError)?;
let board_url = bat_config.miro_board_url.clone();
let board_id = extract_board_id(&board_url)?;
Ok(MiroApiConfig {
access_token: bat_auditor_config.miro_oauth_access_token,
board_id,
board_url,
})
}
fn extract_board_id(board_url: &str) -> EvmMiroResult<String> {
board_url
.split("/board/")
.nth(1)
.and_then(|s| s.split('/').next())
.map(|s| s.trim_end_matches('=').to_string() + "=")
.ok_or_else(|| {
Report::new(EvmMiroError)
.attach_printable(format!("Cannot extract board_id from URL: {}", board_url))
})
}
fn build_frame_url(board_url: &str, frame_id: &str) -> String {
format!("{}/?moveToWidget={}", board_url, frame_id)
}
async fn verify_frame_exists(config: &MiroApiConfig, frame_id: &str) -> bool {
let client = reqwest::Client::new();
let resp = client
.get(format!(
"https://api.miro.com/v2/boards/{}/items/{}",
config.board_id, frame_id
))
.header(AUTHORIZATION, format!("Bearer {}", config.access_token))
.send()
.await;
matches!(resp, Ok(r) if r.status().is_success())
}
async fn deploy_single_frame(
config: &MiroApiConfig,
entry_point_name: &str,
index: usize,
) -> EvmMiroResult<(String, String)> {
let frame_name = format!("co: {}", entry_point_name);
let client = reqwest::Client::new();
let response = client
.post(format!(
"https://api.miro.com/v2/boards/{}/frames",
config.board_id
))
.header(CONTENT_TYPE, "application/json")
.header(AUTHORIZATION, format!("Bearer {}", config.access_token))
.body(
json!({
"data": {
"format": "custom",
"title": frame_name,
"type": "freeform"
},
"position": {
"origin": "center",
"x": 0,
"y": 0
},
"geometry": {
"width": MIRO_FRAME_WIDTH,
"height": MIRO_FRAME_HEIGHT
}
})
.to_string(),
)
.send()
.await
.map_err(|e| {
Report::new(EvmMiroError).attach_printable(format!("HTTP error creating frame: {}", e))
})?;
let body = response.text().await.map_err(|e| {
Report::new(EvmMiroError).attach_printable(format!("Cannot read response body: {}", e))
})?;
let value: Value = serde_json::from_str(&body).map_err(|e| {
Report::new(EvmMiroError).attach_printable(format!("Cannot parse Miro response: {}", e))
})?;
let frame_id = value["id"]
.as_str()
.ok_or_else(|| Report::new(EvmMiroError).attach_printable("No 'id' in Miro response"))?
.to_string();
let x_modifier = index as i64 % MIRO_BOARD_COLUMNS;
let y_modifier = index as i64 / MIRO_BOARD_COLUMNS;
let x_position = MIRO_INITIAL_X + (MIRO_FRAME_WIDTH as i64 + 200) * x_modifier;
let y_position = MIRO_INITIAL_Y + (MIRO_FRAME_HEIGHT as i64 + 400) * y_modifier;
client
.patch(format!(
"https://api.miro.com/v2/boards/{}/frames/{}",
config.board_id, frame_id
))
.header(CONTENT_TYPE, "application/json")
.header(AUTHORIZATION, format!("Bearer {}", config.access_token))
.body(
json!({
"position": {
"x": x_position,
"y": y_position,
"origin": "center"
}
})
.to_string(),
)
.send()
.await
.map_err(|e| {
Report::new(EvmMiroError)
.attach_printable(format!("HTTP error updating position: {}", e))
})?;
let frame_url = build_frame_url(&config.board_url, &frame_id);
Ok((frame_id, frame_url))
}
pub fn get_entry_point_names() -> EvmMiroResult<Vec<String>> {
let evm_metadata = EvmBatMetadata::read_metadata().change_context(EvmMiroError)?;
let mut names: Vec<String> = evm_metadata
.entry_points
.iter()
.map(|ep| ep.name.clone())
.collect();
names.sort();
Ok(names)
}
fn find_function_end_line(file_path: &str, start_line: usize) -> usize {
let content = std::fs::read_to_string(file_path).unwrap_or_default();
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let start_idx = if start_line > 0 { start_line - 1 } else { 0 };
let mut depth: i32 = 0;
let mut found_open = false;
for i in start_idx..total {
for ch in lines[i].chars() {
if ch == '{' {
depth += 1;
found_open = true;
} else if ch == '}' {
depth -= 1;
if found_open && depth == 0 {
return i + 1; }
}
}
}
(start_line + 20).min(total)
}
fn collect_inheritance_chain<'a>(
evm_metadata: &'a EvmBatMetadata,
contract_name: &str,
) -> Vec<&'a crate::batbelt::evm::metadata::bat_metadata::ContractMetadata> {
let mut chain = Vec::new();
let mut visited: HashSet<String> = HashSet::new();
let mut queue: VecDeque<String> = VecDeque::new();
queue.push_back(contract_name.to_string());
while let Some(name) = queue.pop_front() {
if visited.contains(&name) {
continue;
}
visited.insert(name.clone());
if let Some(c) = evm_metadata.get_contract_by_name(&name) {
chain.push(c);
for base in &c.base_contracts {
queue.push_back(base.clone());
}
}
}
chain
}
fn resolve_evm_function_deps(
evm_metadata: &EvmBatMetadata,
contract_name: &str,
func_metadata_id: &str,
func_modifiers: &[String],
_func_lines: &[String],
_contract_file_path: &str,
) -> Vec<(String, String, String, usize, usize)> {
let mut deps: Vec<(String, String, String, usize, usize)> = Vec::new();
let mut seen_names: HashSet<String> = HashSet::new();
let mut seen_ids: HashSet<String> = HashSet::new();
let chain = collect_inheritance_chain(evm_metadata, contract_name);
for mod_name in func_modifiers {
for c in &chain {
if let Some(mod_def) = c.modifiers.iter().find(|m| m.name == *mod_name) {
let mod_id = format!("modifier_{}_{}", c.name, mod_name);
if !seen_ids.contains(&mod_id) {
seen_ids.insert(mod_id.clone());
seen_names.insert(mod_name.clone());
let end = find_function_end_line(&c.file_path, mod_def.line);
deps.push((
mod_id,
format!("modifier {}", mod_name),
c.file_path.clone(),
mod_def.line,
end,
));
}
break;
}
}
}
let called_names: HashSet<String> = evm_metadata
.function_dependencies
.iter()
.find(|d| d.function_metadata_id == func_metadata_id)
.map(|d| d.callees.iter().cloned().collect())
.unwrap_or_default();
for c in &chain {
for func_meta in &c.functions {
if func_meta.metadata_id == func_metadata_id {
continue;
}
if seen_names.contains(&func_meta.name) {
continue;
}
if called_names.contains(&func_meta.name) && !seen_ids.contains(&func_meta.metadata_id)
{
seen_ids.insert(func_meta.metadata_id.clone());
seen_names.insert(func_meta.name.clone());
let end = if func_meta.end_line > 0 {
func_meta.end_line
} else {
find_function_end_line(&c.file_path, func_meta.line)
};
deps.push((
func_meta.metadata_id.clone(),
func_meta.name.clone(),
c.file_path.clone(),
func_meta.line,
end,
));
}
}
}
deps
}
fn find_doc_start_line(file_path: &str, func_start_line: usize) -> usize {
let content = std::fs::read_to_string(file_path).unwrap_or_default();
let lines: Vec<&str> = content.lines().collect();
if func_start_line <= 1 {
return func_start_line;
}
let mut doc_start = func_start_line; let mut i = func_start_line - 2;
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed.is_empty() {
if i == 0 {
break;
}
i -= 1;
} else {
break;
}
}
if i >= lines.len() {
return doc_start;
}
let trimmed = lines[i].trim();
if trimmed.ends_with("*/") {
loop {
doc_start = i + 1; if lines[i].trim().starts_with("/**") || lines[i].trim().starts_with("/*") {
break;
}
if i == 0 {
break;
}
i -= 1;
}
} else if trimmed.starts_with("///") {
doc_start = i + 1;
while i > 0 {
i -= 1;
if lines[i].trim().starts_with("///") {
doc_start = i + 1;
} else {
break;
}
}
}
doc_start
}
pub async fn deploy_source_code_screenshots(selected_miro_frame: MiroFrame) -> EvmMiroResult<()> {
let evm_metadata = EvmBatMetadata::read_metadata().change_context(EvmMiroError)?;
let mut continue_selection = true;
while continue_selection {
let source_options = vec![
"src (project contracts)".to_string(),
"lib (external dependencies)".to_string(),
];
let prompt_text = format!("Select {}", "source location".green());
let source_selection =
crate::batbelt::bat_dialoguer::select(&prompt_text, source_options, None).unwrap();
let is_external = source_selection == 1;
let mut filtered_contracts: Vec<_> = evm_metadata
.contracts
.iter()
.filter(|c| c.external == is_external)
.collect();
filtered_contracts.sort_by(|a, b| a.name.cmp(&b.name));
if filtered_contracts.is_empty() {
println!(
" {} No contracts found in {}",
"Warning:".bright_yellow(),
if is_external { "lib" } else { "src" }
);
continue_selection = crate::batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {} in the {} frame?",
"continue creating screenshots".yellow(),
selected_miro_frame.title.yellow()
))
.unwrap();
continue;
}
let contract_names: Vec<String> = filtered_contracts
.iter()
.map(|c| format!("{} ({})", c.name, c.file_path))
.collect();
let prompt_text = format!("Select {}", "contract".green());
let contract_selection =
BatDialoguer::fuzzy_select(prompt_text, contract_names).change_context(EvmMiroError)?;
let selected_contract = filtered_contracts[contract_selection];
let mut item_labels: Vec<String> = Vec::new();
enum ContractItem {
Function(usize),
StorageVar(usize),
Event(usize),
Modifier(usize),
}
let mut item_refs: Vec<ContractItem> = Vec::new();
for (i, func) in selected_contract.functions.iter().enumerate() {
if func.is_constructor {
continue;
}
item_labels.push(format!("[fn] {}", func.name));
item_refs.push(ContractItem::Function(i));
}
for (i, var) in selected_contract.state_variables.iter().enumerate() {
item_labels.push(format!("[var] {}", var.name));
item_refs.push(ContractItem::StorageVar(i));
}
for (i, event) in selected_contract.events.iter().enumerate() {
item_labels.push(format!("[event] {}", event.name));
item_refs.push(ContractItem::Event(i));
}
for (i, modifier) in selected_contract.modifiers.iter().enumerate() {
item_labels.push(format!("[mod] {}", modifier.name));
item_refs.push(ContractItem::Modifier(i));
}
if item_labels.is_empty() {
println!(
" {} No items found in contract {}",
"Warning:".bright_yellow(),
selected_contract.name
);
continue_selection = crate::batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {} in the {} frame?",
"continue creating screenshots".yellow(),
selected_miro_frame.title.yellow()
))
.unwrap();
continue;
}
let prompt_text = format!(
"Select {} from {}",
"items to screenshot".green(),
selected_contract.name.green()
);
let selections = BatDialoguer::multiselect(
prompt_text,
item_labels.clone(),
Some(&vec![false; item_labels.len()]),
true,
)
.unwrap();
if selections.is_empty() {
continue_selection = crate::batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {} in the {} frame?",
"continue creating screenshots".yellow(),
selected_miro_frame.title.yellow()
))
.unwrap();
continue;
}
let screenshot_options = SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: None,
filters: None,
show_line_number: true,
};
for &sel_idx in &selections {
let item = &item_refs[sel_idx];
let (name, start_line, end_line) = match item {
ContractItem::Function(i) => {
let f = &selected_contract.functions[*i];
let end = if f.end_line > 0 {
f.end_line
} else {
find_function_end_line(&selected_contract.file_path, f.line)
};
(
format!("{}.{}.js", selected_contract.name, f.name),
f.line,
end,
)
}
ContractItem::StorageVar(i) => {
let v = &selected_contract.state_variables[*i];
(
format!("{}.{}.js", selected_contract.name, v.name),
v.line,
v.line,
)
}
ContractItem::Event(i) => {
let e = &selected_contract.events[*i];
(
format!("{}.{}.js", selected_contract.name, e.name),
e.line,
e.line,
)
}
ContractItem::Modifier(i) => {
let m = &selected_contract.modifiers[*i];
let end = if m.end_line > 0 {
m.end_line
} else {
m.line + m.body_source.lines().count()
};
(
format!("{}.{}.js", selected_contract.name, m.name),
m.line,
end,
)
}
};
let base_name = name.trim_end_matches(".js");
let screenshot_name = format!(
"{}::frame={}.js",
base_name,
selected_miro_frame
.title
.replace([' ', '-'], "_")
.to_uppercase()
);
let sc = SourceCodeParser::new(
screenshot_name,
selected_contract.file_path.clone(),
start_line,
end_line,
);
sc.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
0,
selected_miro_frame.height as i64,
screenshot_options.clone(),
)
.await
.change_context(EvmMiroError)?;
}
let prompt_text = format!(
"Do you want to {} in the {} frame?",
"continue creating screenshots".yellow(),
selected_miro_frame.title.yellow()
);
continue_selection = crate::batbelt::bat_dialoguer::select_yes_or_no(&prompt_text).unwrap();
}
Ok(())
}
pub async fn deploy_co_screenshots(entry_point_name: &str) -> EvmMiroResult<()> {
MiroConfig::check_miro_enabled().change_context(EvmMiroError)?;
let evm_metadata = EvmBatMetadata::read_metadata().change_context(EvmMiroError)?;
let frame_ref = evm_metadata
.get_miro_frame_by_ep_name(entry_point_name)
.ok_or_else(|| {
Report::new(EvmMiroError).attach_printable(format!(
"No Miro frame found for '{}'. Run miro code-overhaul-frames first.",
entry_point_name
))
})?
.clone();
if frame_ref.images_deployed {
println!(
" Screenshots already deployed for {}",
entry_point_name.green()
);
return Ok(());
}
let co_miro_frame = MiroFrame::new_from_item_id(&frame_ref.frame_id)
.await
.change_context(EvmMiroError)?;
println!(
"Deploying screenshots for {} to frame {}",
entry_point_name.green(),
co_miro_frame.title.green()
);
let ep = evm_metadata
.get_entry_point_by_name(entry_point_name)
.ok_or_else(|| {
Report::new(EvmMiroError)
.attach_printable(format!("Entry point '{}' not found", entry_point_name))
})?
.clone();
let contract = evm_metadata
.get_contract_by_name(&ep.contract_name)
.ok_or_else(|| {
Report::new(EvmMiroError)
.attach_printable(format!("Contract '{}' not found", ep.contract_name))
})?
.clone();
let func = evm_metadata
.get_function_by_id(&ep.function_metadata_id)
.ok_or_else(|| {
Report::new(EvmMiroError)
.attach_printable(format!("Function '{}' not found", ep.function_metadata_id))
})?
.clone();
let ep_start_line = find_doc_start_line(&contract.file_path, func.line);
let ep_end_line = if func.end_line > 0 {
func.end_line
} else {
find_function_end_line(&contract.file_path, func.line)
};
let ep_sc = SourceCodeParser::new(
format!("ep_{}.js", func.name),
contract.file_path.clone(),
ep_start_line,
ep_end_line,
);
let ep_image = ep_sc
.deploy_screenshot_to_miro_frame(
co_miro_frame.clone(),
1200,
600,
SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: Some(28),
filters: None,
show_line_number: true,
},
)
.await
.change_context(EvmMiroError)?;
let entry_point_image_id = ep_image.item_id.clone();
let mut deployed_function_ids: HashSet<String> = HashSet::new();
deployed_function_ids.insert(ep.function_metadata_id.clone());
let mut deployed_function_names: HashSet<String> = HashSet::new();
deployed_function_names.insert(func.name.clone());
let mut id_to_image: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
id_to_image.insert(
ep.function_metadata_id.clone(),
entry_point_image_id.clone(),
);
let mut bfs_queue: VecDeque<(String, String, Vec<String>, Vec<String>, String)> =
VecDeque::new();
let ep_body_lines: Vec<String> = {
let file_content = std::fs::read_to_string(&contract.file_path).unwrap_or_default();
let lines: Vec<&str> = file_content.lines().collect();
let start = if func.line > 0 { func.line - 1 } else { 0 };
let end = ep_end_line.min(lines.len());
lines[start..end].iter().map(|s| s.to_string()).collect()
};
bfs_queue.push_back((
ep.function_metadata_id.clone(),
func.name.clone(),
func.modifiers.clone(),
ep_body_lines,
contract.file_path.clone(),
));
let mut dependency_image_ids: Vec<String> = Vec::new();
const CASCADE_START_X: i64 = 2400;
const CASCADE_START_Y: i64 = 900;
const CASCADE_STEP: i64 = 60;
const DEP_ARROW_COLORS: &[(&str, &str)] = &[
("#ff0000", "red"),
("#0000ff", "blue"),
("#00aa00", "green"),
("#ff8800", "orange"),
("#aa00ff", "purple"),
("#00cccc", "teal"),
("#ff00aa", "pink"),
("#888800", "olive"),
];
let mut color_index: usize = 0;
while let Some((caller_id, caller_name, caller_modifiers, caller_body, caller_file)) =
bfs_queue.pop_front()
{
let new_dep_functions = resolve_evm_function_deps(
&evm_metadata,
&ep.contract_name,
&caller_id,
&caller_modifiers,
&caller_body,
&caller_file,
);
let new_deps: Vec<_> = new_dep_functions
.into_iter()
.filter(|(id, name, _, _, _)| {
!deployed_function_ids.contains(id) && !deployed_function_names.contains(name)
})
.collect();
if new_deps.is_empty() {
continue;
}
let (arrow_hex, arrow_name) = DEP_ARROW_COLORS[color_index % DEP_ARROW_COLORS.len()];
color_index += 1;
let prompt = format!(
"Press Enter to deploy {} dependencies of `{}` (arrow color: {})",
new_deps.len(),
caller_name,
arrow_name
);
BatDialoguer::input_with_default(prompt, "".to_string()).change_context(EvmMiroError)?;
for (idx, (dep_id, dep_name, dep_path, dep_line, dep_end)) in new_deps.iter().enumerate() {
let dep_sc = SourceCodeParser::new(
format!("dep_{}.js", dep_name),
dep_path.clone(),
*dep_line,
*dep_end,
);
let cascade_x = CASCADE_START_X + (idx as i64) * CASCADE_STEP;
let cascade_y = CASCADE_START_Y + (idx as i64) * CASCADE_STEP;
let dep_image = dep_sc
.deploy_screenshot_to_miro_frame(
co_miro_frame.clone(),
cascade_x,
cascade_y,
SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: Some(16),
filters: None,
show_line_number: true,
},
)
.await
.change_context(EvmMiroError)?;
if let Some(caller_image_id) = id_to_image.get(&caller_id) {
create_connector_with_color(
&dep_image.item_id,
caller_image_id,
None,
Some(arrow_hex),
)
.await
.change_context(EvmMiroError)?;
}
id_to_image.insert(dep_id.clone(), dep_image.item_id.clone());
deployed_function_ids.insert(dep_id.clone());
let base_name = dep_name.strip_prefix("modifier ").unwrap_or(dep_name);
deployed_function_names.insert(base_name.to_string());
dependency_image_ids.push(dep_image.item_id.clone());
let dep_body: Vec<String> = {
let fc = std::fs::read_to_string(dep_path).unwrap_or_default();
let lines: Vec<&str> = fc.lines().collect();
let s = if *dep_line > 0 { dep_line - 1 } else { 0 };
let e = (*dep_end).min(lines.len());
lines[s..e].iter().map(|l| l.to_string()).collect()
};
bfs_queue.push_back((
dep_id.clone(),
dep_name.clone(),
vec![],
dep_body,
dep_path.clone(),
));
}
}
EvmBatMetadata::update_metadata(|metadata| {
if let Some(frame) = metadata
.miro
.frames
.iter_mut()
.find(|f| f.entry_point_name == entry_point_name)
{
frame.images_deployed = true;
frame.entry_point_image_id = entry_point_image_id.clone();
frame.validations_image_id = String::new();
frame.dependency_image_ids = dependency_image_ids.clone();
}
})
.change_context(EvmMiroError)?;
if let Some(ref frame_url) = co_miro_frame.frame_url {
let co_file_name = format!("{}.md", entry_point_name);
let co_bat_file = crate::batbelt::path::BatFile::CodeOverhaulStarted {
file_name: co_file_name,
program_name: None,
};
if let Ok(content) = co_bat_file.read_content(true) {
let placeholder = "`COMPLETE_WITH_MIRO_FRAME_URL`";
if content.contains(placeholder) {
let new_content = content.replace(placeholder, frame_url);
let _ = co_bat_file.write_content(true, &new_content);
}
}
}
println!(" Screenshots deployed for {}", entry_point_name.green());
Ok(())
}