diff --git a/crates/brush-core-vendored/src/pathsearch.rs b/crates/brush-core-vendored/src/pathsearch.rs index 4e3fdefc5..1d4fbf6f4 100644 --- a/crates/brush-core-vendored/src/pathsearch.rs +++ b/crates/brush-core-vendored/src/pathsearch.rs @@ -2,30 +2,51 @@ use std::{collections::VecDeque, path::PathBuf}; + use crate::sys::fs::PathExt; /// Encapsulates the result of a path search. -pub struct ExecutablePathSearch +pub struct ExecutablePathSearch where PI: AsRef, - N: AsRef, { paths: VecDeque, - filename: N, + filenames: Vec, } -impl Iterator for ExecutablePathSearch +#[cfg(windows)] +fn candidate_filenames(filename: &str) -> Vec { + if std::path::Path::new(filename).extension().is_some() { + return vec![filename.to_string()]; + } + + let mut candidates = Vec::new(); + candidates.push(filename.to_string()); + for ext in crate::sys::fs::executable_extensions() { + candidates.push(format!("{filename}{ext}")); + } + candidates +} + +#[cfg(not(windows))] +fn candidate_filenames(filename: &str) -> Vec { + vec![filename.to_string()] +} + +impl Iterator for ExecutablePathSearch where PI: AsRef, - N: AsRef, { type Item = PathBuf; fn next(&mut self) -> Option { while let Some(path) = self.paths.pop_front() { - let path = PathBuf::from(path.as_ref()).join(self.filename.as_ref()); - if path.is_file() && path.as_path().executable() { - return Some(path); + let base_path = PathBuf::from(path.as_ref()); + for filename in &self.filenames { + let path = base_path.join(filename); + if path.is_file() && path.as_path().executable() { + return Some(path); + } } } @@ -95,15 +116,16 @@ where /// /// * `paths` - An iterator over the paths to search. /// * `filename` - The name of the executable file to search for. -pub fn search_for_executable(paths: P, filename: N) -> ExecutablePathSearch +pub fn search_for_executable(paths: P, filename: N) -> ExecutablePathSearch where P: Iterator, PI: AsRef, N: AsRef, { + let filenames = candidate_filenames(filename.as_ref()); ExecutablePathSearch { paths: paths.collect(), - filename, + filenames, } } diff --git a/crates/brush-core-vendored/src/shell.rs b/crates/brush-core-vendored/src/shell.rs index 9813a8cf5..3019e2006 100644 --- a/crates/brush-core-vendored/src/shell.rs +++ b/crates/brush-core-vendored/src/shell.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::ffi::OsStr; use std::collections::HashMap; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; @@ -12,7 +13,6 @@ use crate::env::{EnvironmentLookup, EnvironmentScope, ShellEnvironment}; use crate::interp::{self, Execute, ExecutionParameters}; use crate::options::RuntimeOptions; use crate::results::ExecutionSpawnResult; -use crate::sys::fs::PathExt; use crate::variables::{self, ShellVariable}; use crate::{ ExecutionControlFlow, ExecutionExitCode, ExecutionResult, ProcessGroupPolicy, history, @@ -1286,7 +1286,8 @@ impl Shell { filename: &'a str, ) -> impl Iterator + 'a { let path_var = self.env.get_str("PATH", self).unwrap_or_default(); - let paths = path_var.split(':').map(|s| s.to_owned()); + let paths = std::env::split_paths(OsStr::new(path_var.as_ref())) + .map(|path| path.to_string_lossy().to_string()); pathsearch::search_for_executable(paths.into_iter(), filename) } @@ -1303,7 +1304,8 @@ impl Shell { case_insensitive: bool, ) -> impl Iterator { let path_var = self.env.get_str("PATH", self).unwrap_or_default(); - let paths = path_var.split(':').map(|s| s.to_owned()); + let paths = std::env::split_paths(OsStr::new(path_var.as_ref())) + .map(|path| path.to_string_lossy().to_string()); pathsearch::search_for_executable_with_prefix( paths.into_iter(), @@ -1322,13 +1324,10 @@ impl Shell { &self, candidate_name: S, ) -> Option { - for dir_str in self.env_str("PATH").unwrap_or_default().split(':') { - let candidate_path = Path::new(dir_str).join(candidate_name.as_ref()); - if candidate_path.executable() { - return Some(candidate_path); - } - } - None + let path_var = self.env_str("PATH").unwrap_or_default(); + let paths = std::env::split_paths(OsStr::new(path_var.as_ref())) + .map(|path| path.to_string_lossy().to_string()); + pathsearch::search_for_executable(paths.into_iter(), candidate_name.as_ref()).next() } /// Uses the shell's hash-based path cache to check whether the given filename is the name diff --git a/crates/brush-core-vendored/src/sys/windows/fs.rs b/crates/brush-core-vendored/src/sys/windows/fs.rs index 4b457e1e9..5e9a36377 100644 --- a/crates/brush-core-vendored/src/sys/windows/fs.rs +++ b/crates/brush-core-vendored/src/sys/windows/fs.rs @@ -1,6 +1,7 @@ //! Filesystem utilities. use crate::error; +use std::{ffi::OsStr, sync::OnceLock}; impl crate::sys::fs::PathExt for std::path::Path { fn readable(&self) -> bool { @@ -12,7 +13,16 @@ impl crate::sys::fs::PathExt for std::path::Path { } fn executable(&self) -> bool { - true + if !self.is_file() { + return false; + } + let Some(ext) = self.extension().and_then(OsStr::to_str) else { + return false; + }; + let ext = format!(".{ext}"); + executable_extensions() + .iter() + .any(|known| known.eq_ignore_ascii_case(&ext)) } fn exists_and_is_block_device(&self) -> bool { @@ -76,3 +86,35 @@ pub fn open_null_file() -> Result { Ok(f) } + +pub(crate) fn executable_extensions() -> &'static [String] { + static PATHEXT: OnceLock> = OnceLock::new(); + PATHEXT.get_or_init(|| { + let fallback = [".COM", ".EXE", ".BAT", ".CMD"]; + let Some(value) = std::env::var_os("PATHEXT") else { + return fallback.iter().map(|ext| (*ext).to_string()).collect(); + }; + + let value = value.to_string_lossy(); + let mut exts = value + .split(';') + .filter_map(|ext| { + let ext = ext.trim(); + if ext.is_empty() { + None + } else if ext.starts_with('.') { + Some(ext.to_string()) + } else { + Some(format!(".{ext}")) + } + }) + .collect::>(); + + if exts.is_empty() { + exts = fallback.iter().map(|ext| (*ext).to_string()).collect::>(); + return exts; + } + + exts + }) +} diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index bed08f876..63e93c04a 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -209,4 +209,4 @@ tasks: [ ``` When the set of files isn't known upfront, grep/glob yourself first to enumerate them, then fan out one task per file. - + \ No newline at end of file diff --git a/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts b/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts index 4ee8e2618..ef97acc0e 100644 --- a/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; -import { PythonKernel } from "@oh-my-pi/pi-coding-agent/ipy/kernel"; import * as gatewayCoordinator from "@oh-my-pi/pi-coding-agent/ipy/gateway-coordinator"; +import { PythonKernel } from "@oh-my-pi/pi-coding-agent/ipy/kernel"; import { TempDir } from "@oh-my-pi/pi-utils"; import type { Subprocess } from "bun";