use std::{ collections::{HashMap, HashSet}, fmt::Display, path::{Path, PathBuf}, sync::Arc,}; use gpui::AsyncWindowContext;use node_runtime::NodeRuntime;use serde::Deserialize;use settings::{DevContainerConnection, Settings as _};use smol::{fs, process::Command};use util::rel_path::RelPath;use workspace::Workspace; use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate}; /// Represents a discovered devcontainer configuration#[derive(Debug, Clone, PartialEq, Eq)]pub struct DevContainerConfig { /// Display name for the configuration (subfolder name or "default") pub name: String, /// Relative path to the devcontainer.json file from the project root pub config_path: PathBuf,} impl DevContainerConfig { pub fn default_config() -> Self { Self { name: "default".to_string(), config_path: PathBuf::from(".devcontainer/devcontainer.json"), } }} #[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]struct DevContainerUp { _outcome: String, container_id: String, remote_user: String, remote_workspace_folder: String,} #[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub(crate) struct DevContainerApply { pub(crate) files: Vec<String>,} #[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub(crate) struct DevContainerConfiguration { name: Option<String>,} #[derive(Debug, Deserialize)]pub(crate) struct DevContainerConfigurationOutput { configuration: DevContainerConfiguration,} #[derive(Debug, Clone, PartialEq, Eq)]pub enum DevContainerError { DockerNotAvailable, DevContainerCliNotAvailable, DevContainerTemplateApplyFailed(String), DevContainerUpFailed(String), DevContainerNotFound, DevContainerParseFailed, NodeRuntimeNotAvailable, NotInValidProject,} impl Display for DevContainerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { DevContainerError::DockerNotAvailable => "docker CLI not found on $PATH".to_string(), DevContainerError::DevContainerCliNotAvailable => "devcontainer CLI not found on path".to_string(), DevContainerError::DevContainerUpFailed(_) => { "DevContainer creation failed".to_string() } DevContainerError::DevContainerTemplateApplyFailed(_) => { "DevContainer template apply failed".to_string() } DevContainerError::DevContainerNotFound => "No valid dev container definition found in project".to_string(), DevContainerError::DevContainerParseFailed => "Failed to parse file .devcontainer/devcontainer.json".to_string(), DevContainerError::NodeRuntimeNotAvailable => "Cannot find a valid node runtime".to_string(), DevContainerError::NotInValidProject => "Not within a valid project".to_string(), } ) }} pub(crate) async fn read_devcontainer_configuration_for_project( cx: &mut AsyncWindowContext, node_runtime: &NodeRuntime,) -> Result<DevContainerConfigurationOutput, DevContainerError> { let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; let Some(directory) = project_directory(cx) else { return Err(DevContainerError::NotInValidProject); }; devcontainer_read_configuration( &path_to_devcontainer_cli, found_in_path, node_runtime, &directory, None, use_podman(cx), ) .await} pub(crate) async fn apply_dev_container_template( template: &DevContainerTemplate, options_selected: &HashMap<String, String>, features_selected: &HashSet<DevContainerFeature>, cx: &mut AsyncWindowContext, node_runtime: &NodeRuntime,) -> Result<DevContainerApply, DevContainerError> { let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; let Some(directory) = project_directory(cx) else { return Err(DevContainerError::NotInValidProject); }; devcontainer_template_apply( template, options_selected, features_selected, &path_to_devcontainer_cli, found_in_path, node_runtime, &directory, false, // devcontainer template apply does not use --docker-path option ) .await} fn use_podman(cx: &mut AsyncWindowContext) -> bool { cx.update(|_, cx| DevContainerSettings::get_global(cx).use_podman) .unwrap_or(false)} /// Finds all available devcontainer configurations in the project.////// This function scans for:/// 1. `.devcontainer/devcontainer.json` (the default location)/// 2. `.devcontainer/<subfolder>/devcontainer.json` (named configurations)////// Returns a list of found configurations, or an empty list if none are found.pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> { let Some(workspace) = cx.window_handle().downcast::<Workspace>() else { log::debug!("find_devcontainer_configs: No workspace found"); return Vec::new(); }; let Ok(configs) = workspace.update(cx, |workspace, _, cx| { let project = workspace.project().read(cx); let worktree = project .visible_worktrees(cx) .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); let Some(worktree) = worktree else { log::debug!("find_devcontainer_configs: No worktree found"); return Vec::new(); }; let worktree = worktree.read(cx); let mut configs = Vec::new(); let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path"); let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else { log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree"); return Vec::new(); }; if !devcontainer_entry.is_dir() { log::debug!("find_devcontainer_configs: .devcontainer is not a directory"); return Vec::new(); } log::debug!("find_devcontainer_configs: Scanning .devcontainer directory"); let devcontainer_json_path = RelPath::unix(".devcontainer/devcontainer.json").expect("valid path"); for entry in worktree.child_entries(devcontainer_path) { log::debug!( "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}", entry.path.as_unix_str(), entry.is_file(), entry.is_dir() ); if entry.is_file() && entry.path.as_ref() == devcontainer_json_path { log::debug!("find_devcontainer_configs: Found default devcontainer.json"); configs.push(DevContainerConfig::default_config()); } else if entry.is_dir() { let subfolder_name = entry .path .file_name() .map(|n| n.to_string()) .unwrap_or_default(); let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str()); if let Ok(rel_config_path) = RelPath::unix(&config_json_path) { if worktree.entry_for_path(rel_config_path).is_some() { log::debug!( "find_devcontainer_configs: Found config in subfolder: {}", subfolder_name ); configs.push(DevContainerConfig { name: subfolder_name, config_path: PathBuf::from(&config_json_path), }); } else { log::debug!( "find_devcontainer_configs: Subfolder {} has no devcontainer.json", subfolder_name ); } } } } log::info!( "find_devcontainer_configs: Found {} configurations", configs.len() ); configs.sort_by(|a, b| { if a.name == "default" { std::cmp::Ordering::Less } else if b.name == "default" { std::cmp::Ordering::Greater } else { a.name.cmp(&b.name) } }); configs }) else { log::debug!("find_devcontainer_configs: Failed to update workspace"); return Vec::new(); }; configs} pub async fn start_dev_container_with_config( cx: &mut AsyncWindowContext, node_runtime: NodeRuntime, config: Option<DevContainerConfig>,) -> Result<(DevContainerConnection, String), DevContainerError> { let use_podman = use_podman(cx); check_for_docker(use_podman).await?; let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; let Some(directory) = project_directory(cx) else { return Err(DevContainerError::NotInValidProject); }; let config_path = config.map(|c| directory.join(&c.config_path)); match devcontainer_up( &path_to_devcontainer_cli, found_in_path, &node_runtime, directory.clone(), config_path.clone(), use_podman, ) .await { Ok(DevContainerUp { container_id, remote_workspace_folder, remote_user, .. }) => { let project_name = match devcontainer_read_configuration( &path_to_devcontainer_cli, found_in_path, &node_runtime, &directory, config_path.as_ref(), use_podman, ) .await { Ok(DevContainerConfigurationOutput { configuration: DevContainerConfiguration { name: Some(project_name), }, }) => project_name, _ => get_backup_project_name(&remote_workspace_folder, &container_id), }; let connection = DevContainerConnection { name: project_name, container_id: container_id, use_podman, remote_user, }; Ok((connection, remote_workspace_folder)) } Err(err) => { let message = format!("Failed with nested error: {}", err); Err(DevContainerError::DevContainerUpFailed(message)) } }} #[cfg(not(target_os = "windows"))]fn dev_container_cli() -> String { "devcontainer".to_string()} #[cfg(target_os = "windows")]fn dev_container_cli() -> String { "devcontainer.cmd".to_string()} async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> { let mut command = if use_podman { util::command::new_smol_command("podman") } else { util::command::new_smol_command("docker") }; command.arg("--version"); match command.output().await { Ok(_) => Ok(()), Err(e) => { log::error!("Unable to find docker in $PATH: {:?}", e); Err(DevContainerError::DockerNotAvailable) } }} async fn ensure_devcontainer_cli( node_runtime: &NodeRuntime,) -> Result<(PathBuf, bool), DevContainerError> { let mut command = util::command::new_smol_command(&dev_container_cli()); command.arg("--version"); if let Err(e) = command.output().await { log::error!( "Unable to find devcontainer CLI in $PATH. Checking for a zed installed version. Error: {:?}", e ); let Ok(node_runtime_path) = node_runtime.binary_path().await else { return Err(DevContainerError::NodeRuntimeNotAvailable); }; let datadir_cli_path = paths::devcontainer_dir() .join("node_modules") .join("@devcontainers") .join("cli") .join(format!("{}.js", &dev_container_cli())); log::debug!( "devcontainer not found in path, using local location: ${}", datadir_cli_path.display() ); let mut command = util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string()); command.arg(datadir_cli_path.display().to_string()); command.arg("--version"); match command.output().await { Err(e) => log::error!( "Unable to find devcontainer CLI in Data dir. Will try to install. Error: {:?}", e ), Ok(output) => { if output.status.success() { log::info!("Found devcontainer CLI in Data dir"); return Ok((datadir_cli_path.clone(), false)); } else { log::error!( "Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}", output ); } } } if let Err(e) = fs::create_dir_all(paths::devcontainer_dir()).await { log::error!("Unable to create devcontainer directory. Error: {:?}", e); return Err(DevContainerError::DevContainerCliNotAvailable); } if let Err(e) = node_runtime .npm_install_packages( &paths::devcontainer_dir(), &[("@devcontainers/cli", "latest")], ) .await { log::error!( "Unable to install devcontainer CLI to data directory. Error: {:?}", e ); return Err(DevContainerError::DevContainerCliNotAvailable); }; let mut command = util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string()); command.arg(datadir_cli_path.display().to_string()); command.arg("--version"); if let Err(e) = command.output().await { log::error!( "Unable to find devcontainer cli after NPM install. Error: {:?}", e ); Err(DevContainerError::DevContainerCliNotAvailable) } else { Ok((datadir_cli_path, false)) } } else { log::info!("Found devcontainer cli on $PATH, using it"); Ok((PathBuf::from(&dev_container_cli()), true)) }} async fn devcontainer_up( path_to_cli: &PathBuf, found_in_path: bool, node_runtime: &NodeRuntime, path: Arc<Path>, config_path: Option<PathBuf>, use_podman: bool,) -> Result<DevContainerUp, DevContainerError> { let Ok(node_runtime_path) = node_runtime.binary_path().await else { log::error!("Unable to find node runtime path"); return Err(DevContainerError::NodeRuntimeNotAvailable); }; let mut command = devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman); command.arg("up"); command.arg("--workspace-folder"); command.arg(path.display().to_string()); if let Some(config) = config_path { command.arg("--config"); command.arg(config.display().to_string()); } log::info!("Running full devcontainer up command: {:?}", command); match command.output().await { Ok(output) => { if output.status.success() { let raw = String::from_utf8_lossy(&output.stdout); parse_json_from_cli(&raw) } else { let message = format!( "Non-success status running devcontainer up for workspace: out: {}, err: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); log::error!("{}", &message); Err(DevContainerError::DevContainerUpFailed(message)) } } Err(e) => { let message = format!("Error running devcontainer up: {:?}", e); log::error!("{}", &message); Err(DevContainerError::DevContainerUpFailed(message)) } }} async fn devcontainer_read_configuration( path_to_cli: &PathBuf, found_in_path: bool, node_runtime: &NodeRuntime, path: &Arc<Path>, config_path: Option<&PathBuf>, use_podman: bool,) -> Result<DevContainerConfigurationOutput, DevContainerError> { let Ok(node_runtime_path) = node_runtime.binary_path().await else {Lines 1–500 of 725. Load more
use std::{ collections::{HashMap, HashSet}, fmt::Display, path::{Path, PathBuf}, sync::Arc, }; use gpui::AsyncWindowContext; use node_runtime::NodeRuntime; use serde::Deserialize; use settings::{DevContainerConnection, Settings as _}; use smol::{fs, process::Command}; use util::rel_path::RelPath; use workspace::Workspace; use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate}; /// Represents a discovered devcontainer configuration #[derive(Debug, Clone, PartialEq, Eq)] pub struct DevContainerConfig { /// Display name for the configuration (subfolder name or "default") pub name: String, /// Relative path to the devcontainer.json file from the project root pub config_path: PathBuf, } impl DevContainerConfig { pub fn default_config() -> Self { Self { name: "default".to_string(), config_path: PathBuf::from(".devcontainer/devcontainer.json"), } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DevContainerUp { _outcome: String, container_id: String, remote_user: String, remote_workspace_folder: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct DevContainerApply { pub(crate) files: Vec<String>, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct DevContainerConfiguration { name: Option<String>, } #[derive(Debug, Deserialize)] pub(crate) struct DevContainerConfigurationOutput { configuration: DevContainerConfiguration, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum DevContainerError { DockerNotAvailable, DevContainerCliNotAvailable, DevContainerTemplateApplyFailed(String), DevContainerUpFailed(String), DevContainerNotFound, DevContainerParseFailed, NodeRuntimeNotAvailable, NotInValidProject, } impl Display for DevContainerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { DevContainerError::DockerNotAvailable => "docker CLI not found on $PATH".to_string(), DevContainerError::DevContainerCliNotAvailable => "devcontainer CLI not found on path".to_string(), DevContainerError::DevContainerUpFailed(_) => { "DevContainer creation failed".to_string() } DevContainerError::DevContainerTemplateApplyFailed(_) => { "DevContainer template apply failed".to_string() } DevContainerError::DevContainerNotFound => "No valid dev container definition found in project".to_string(), DevContainerError::DevContainerParseFailed => "Failed to parse file .devcontainer/devcontainer.json".to_string(), DevContainerError::NodeRuntimeNotAvailable => "Cannot find a valid node runtime".to_string(), DevContainerError::NotInValidProject => "Not within a valid project".to_string(), } ) } } pub(crate) async fn read_devcontainer_configuration_for_project( cx: &mut AsyncWindowContext, node_runtime: &NodeRuntime, ) -> Result<DevContainerConfigurationOutput, DevContainerError> { let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; let Some(directory) = project_directory(cx) else { return Err(DevContainerError::NotInValidProject); }; devcontainer_read_configuration( &path_to_devcontainer_cli, found_in_path, node_runtime, &directory, None, use_podman(cx), ) .await } pub(crate) async fn apply_dev_container_template( template: &DevContainerTemplate, options_selected: &HashMap<String, String>, features_selected: &HashSet<DevContainerFeature>, cx: &mut AsyncWindowContext, node_runtime: &NodeRuntime, ) -> Result<DevContainerApply, DevContainerError> { let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; let Some(directory) = project_directory(cx) else { return Err(DevContainerError::NotInValidProject); }; devcontainer_template_apply( template, options_selected, features_selected, &path_to_devcontainer_cli, found_in_path, node_runtime, &directory, false, // devcontainer template apply does not use --docker-path option ) .await } fn use_podman(cx: &mut AsyncWindowContext) -> bool { cx.update(|_, cx| DevContainerSettings::get_global(cx).use_podman) .unwrap_or(false) } /// Finds all available devcontainer configurations in the project. /// /// This function scans for: /// 1. `.devcontainer/devcontainer.json` (the default location) /// 2. `.devcontainer/<subfolder>/devcontainer.json` (named configurations) /// /// Returns a list of found configurations, or an empty list if none are found. pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> { let Some(workspace) = cx.window_handle().downcast::<Workspace>() else { log::debug!("find_devcontainer_configs: No workspace found"); return Vec::new(); }; let Ok(configs) = workspace.update(cx, |workspace, _, cx| { let project = workspace.project().read(cx); let worktree = project .visible_worktrees(cx) .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); let Some(worktree) = worktree else { log::debug!("find_devcontainer_configs: No worktree found"); return Vec::new(); }; let worktree = worktree.read(cx); let mut configs = Vec::new(); let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path"); let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else { log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree"); return Vec::new(); }; if !devcontainer_entry.is_dir() { log::debug!("find_devcontainer_configs: .devcontainer is not a directory"); return Vec::new(); } log::debug!("find_devcontainer_configs: Scanning .devcontainer directory"); let devcontainer_json_path = RelPath::unix(".devcontainer/devcontainer.json").expect("valid path"); for entry in worktree.child_entries(devcontainer_path) { log::debug!( "find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}", entry.path.as_unix_str(), entry.is_file(), entry.is_dir() ); if entry.is_file() && entry.path.as_ref() == devcontainer_json_path { log::debug!("find_devcontainer_configs: Found default devcontainer.json"); configs.push(DevContainerConfig::default_config()); } else if entry.is_dir() { let subfolder_name = entry .path .file_name() .map(|n| n.to_string()) .unwrap_or_default(); let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str()); if let Ok(rel_config_path) = RelPath::unix(&config_json_path) { if worktree.entry_for_path(rel_config_path).is_some() { log::debug!( "find_devcontainer_configs: Found config in subfolder: {}", subfolder_name ); configs.push(DevContainerConfig { name: subfolder_name, config_path: PathBuf::from(&config_json_path), }); } else { log::debug!( "find_devcontainer_configs: Subfolder {} has no devcontainer.json", subfolder_name ); } } } } log::info!( "find_devcontainer_configs: Found {} configurations", configs.len() ); configs.sort_by(|a, b| { if a.name == "default" { std::cmp::Ordering::Less } else if b.name == "default" { std::cmp::Ordering::Greater } else { a.name.cmp(&b.name) } }); configs }) else { log::debug!("find_devcontainer_configs: Failed to update workspace"); return Vec::new(); }; configs } pub async fn start_dev_container_with_config( cx: &mut AsyncWindowContext, node_runtime: NodeRuntime, config: Option<DevContainerConfig>, ) -> Result<(DevContainerConnection, String), DevContainerError> { let use_podman = use_podman(cx); check_for_docker(use_podman).await?; let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?; let Some(directory) = project_directory(cx) else { return Err(DevContainerError::NotInValidProject); }; let config_path = config.map(|c| directory.join(&c.config_path)); match devcontainer_up( &path_to_devcontainer_cli, found_in_path, &node_runtime, directory.clone(), config_path.clone(), use_podman, ) .await { Ok(DevContainerUp { container_id, remote_workspace_folder, remote_user, .. }) => { let project_name = match devcontainer_read_configuration( &path_to_devcontainer_cli, found_in_path, &node_runtime, &directory, config_path.as_ref(), use_podman, ) .await { Ok(DevContainerConfigurationOutput { configuration: DevContainerConfiguration { name: Some(project_name), }, }) => project_name, _ => get_backup_project_name(&remote_workspace_folder, &container_id), }; let connection = DevContainerConnection { name: project_name, container_id: container_id, use_podman, remote_user, }; Ok((connection, remote_workspace_folder)) } Err(err) => { let message = format!("Failed with nested error: {}", err); Err(DevContainerError::DevContainerUpFailed(message)) } } } #[cfg(not(target_os = "windows"))] fn dev_container_cli() -> String { "devcontainer".to_string() } #[cfg(target_os = "windows")] fn dev_container_cli() -> String { "devcontainer.cmd".to_string() } async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> { let mut command = if use_podman { util::command::new_smol_command("podman") } else { util::command::new_smol_command("docker") }; command.arg("--version"); match command.output().await { Ok(_) => Ok(()), Err(e) => { log::error!("Unable to find docker in $PATH: {:?}", e); Err(DevContainerError::DockerNotAvailable) } } } async fn ensure_devcontainer_cli( node_runtime: &NodeRuntime, ) -> Result<(PathBuf, bool), DevContainerError> { let mut command = util::command::new_smol_command(&dev_container_cli()); command.arg("--version"); if let Err(e) = command.output().await { log::error!( "Unable to find devcontainer CLI in $PATH. Checking for a zed installed version. Error: {:?}", e ); let Ok(node_runtime_path) = node_runtime.binary_path().await else { return Err(DevContainerError::NodeRuntimeNotAvailable); }; let datadir_cli_path = paths::devcontainer_dir() .join("node_modules") .join("@devcontainers") .join("cli") .join(format!("{}.js", &dev_container_cli())); log::debug!( "devcontainer not found in path, using local location: ${}", datadir_cli_path.display() ); let mut command = util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string()); command.arg(datadir_cli_path.display().to_string()); command.arg("--version"); match command.output().await { Err(e) => log::error!( "Unable to find devcontainer CLI in Data dir. Will try to install. Error: {:?}", e ), Ok(output) => { if output.status.success() { log::info!("Found devcontainer CLI in Data dir"); return Ok((datadir_cli_path.clone(), false)); } else { log::error!( "Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}", output ); } } } if let Err(e) = fs::create_dir_all(paths::devcontainer_dir()).await { log::error!("Unable to create devcontainer directory. Error: {:?}", e); return Err(DevContainerError::DevContainerCliNotAvailable); } if let Err(e) = node_runtime .npm_install_packages( &paths::devcontainer_dir(), &[("@devcontainers/cli", "latest")], ) .await { log::error!( "Unable to install devcontainer CLI to data directory. Error: {:?}", e ); return Err(DevContainerError::DevContainerCliNotAvailable); }; let mut command = util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string()); command.arg(datadir_cli_path.display().to_string()); command.arg("--version"); if let Err(e) = command.output().await { log::error!( "Unable to find devcontainer cli after NPM install. Error: {:?}", e ); Err(DevContainerError::DevContainerCliNotAvailable) } else { Ok((datadir_cli_path, false)) } } else { log::info!("Found devcontainer cli on $PATH, using it"); Ok((PathBuf::from(&dev_container_cli()), true)) } } async fn devcontainer_up( path_to_cli: &PathBuf, found_in_path: bool, node_runtime: &NodeRuntime, path: Arc<Path>, config_path: Option<PathBuf>, use_podman: bool, ) -> Result<DevContainerUp, DevContainerError> { let Ok(node_runtime_path) = node_runtime.binary_path().await else { log::error!("Unable to find node runtime path"); return Err(DevContainerError::NodeRuntimeNotAvailable); }; let mut command = devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman); command.arg("up"); command.arg("--workspace-folder"); command.arg(path.display().to_string()); if let Some(config) = config_path { command.arg("--config"); command.arg(config.display().to_string()); } log::info!("Running full devcontainer up command: {:?}", command); match command.output().await { Ok(output) => { if output.status.success() { let raw = String::from_utf8_lossy(&output.stdout); parse_json_from_cli(&raw) } else { let message = format!( "Non-success status running devcontainer up for workspace: out: {}, err: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); log::error!("{}", &message); Err(DevContainerError::DevContainerUpFailed(message)) } } Err(e) => { let message = format!("Error running devcontainer up: {:?}", e); log::error!("{}", &message); Err(DevContainerError::DevContainerUpFailed(message)) } } } async fn devcontainer_read_configuration( path_to_cli: &PathBuf, found_in_path: bool, node_runtime: &NodeRuntime, path: &Arc<Path>, config_path: Option<&PathBuf>, use_podman: bool, ) -> Result<DevContainerConfigurationOutput, DevContainerError> { let Ok(node_runtime_path) = node_runtime.binary_path().await else {