refactor: migrated binary detection to $which() utility across codebase
- Replaced all Bun.which() calls with $which() utility from @oh-my-pi/pi-utils across 22 files. - Removed findBashOnPath() wrapper function from procmgr.ts, consolidating binary path resolution. - Updated AGENTS.md documentation to reflect new $which() API usage pattern. - Centralized binary detection logic through shared utility, reducing code duplication.
This commit is contained in:
@@ -339,7 +339,7 @@ const wrapped = Bun.wrapAnsi(text, width, {
|
||||
| File read/write | `Bun.file()`, `Bun.write()` | `readFileSync`, `writeFileSync` |
|
||||
| Spawn process | `$\`cmd\``, `Bun.spawn()` | `child_process` |
|
||||
| Sleep | `Bun.sleep(ms)` | `setTimeout` promise |
|
||||
| Binary lookup | `Bun.which("git")` | `spawnSync(["which", "git"])` |
|
||||
| Binary lookup | `$which("git")` from `@oh-my-pi/pi-utils` | `spawnSync(["which", "git"])` |
|
||||
| HTTP server | `Bun.serve()` | `http.createServer()` |
|
||||
| SQLite | `bun:sqlite` | `better-sqlite3` |
|
||||
| Hashing | `Bun.hash()`, Web Crypto | `node:crypto` |
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
||||
import { complete } from "@oh-my-pi/pi-ai/stream";
|
||||
import type { AssistantMessage, Context, Model, Usage } from "@oh-my-pi/pi-ai/types";
|
||||
import { isContextOverflow } from "@oh-my-pi/pi-ai/utils/overflow";
|
||||
import { $which } from "@oh-my-pi/pi-utils";
|
||||
import { e2eApiKey, resolveApiKey } from "./oauth";
|
||||
|
||||
// Resolve OAuth tokens at module level (async, runs before tests)
|
||||
@@ -445,7 +446,7 @@ describe("Context overflow error handling", () => {
|
||||
// =============================================================================
|
||||
|
||||
// Ollama tests require PI_LOCAL_LLM=1 and ollama installed
|
||||
const ollamaInstalled = !!Bun.env.PI_LOCAL_LLM && !!Bun.which("ollama");
|
||||
const ollamaInstalled = !!Bun.env.PI_LOCAL_LLM && !!$which("ollama");
|
||||
|
||||
describe.skipIf(!ollamaInstalled)("Ollama (local)", () => {
|
||||
let ollamaProcess: ChildProcess | null = null;
|
||||
|
||||
@@ -74,6 +74,7 @@ import * as path from "node:path";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
||||
import { complete } from "@oh-my-pi/pi-ai/stream";
|
||||
import type { Api, Context, ImageContent, Model, OptionsForApi, UserMessage } from "@oh-my-pi/pi-ai/types";
|
||||
import { $which } from "@oh-my-pi/pi-utils";
|
||||
import { e2eApiKey } from "./oauth";
|
||||
|
||||
const TEMP_DIR = path.join(import.meta.dir, ".temp-images");
|
||||
@@ -261,7 +262,7 @@ describe("Image Limits E2E Tests", () => {
|
||||
let smallImage: string; // 100x100 for count tests
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!Bun.which("magick")) return;
|
||||
if (!$which("magick")) return;
|
||||
// Create temp directory
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
||||
import { complete, getEnvApiKey, stream } from "@oh-my-pi/pi-ai/stream";
|
||||
import type { Api, Context, ImageContent, Model, OptionsForApi, Tool, ToolResultMessage } from "@oh-my-pi/pi-ai/types";
|
||||
import { StringEnum } from "@oh-my-pi/pi-ai/utils/schema";
|
||||
import { $which } from "@oh-my-pi/pi-utils";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { e2eApiKey, resolveApiKey } from "./oauth";
|
||||
|
||||
@@ -1509,7 +1510,7 @@ describe("Generate E2E Tests", () => {
|
||||
});
|
||||
|
||||
// Ollama tests require PI_LOCAL_LLM=1 and ollama installed
|
||||
const ollamaInstalled = !!Bun.env.PI_LOCAL_LLM && !!Bun.which("ollama");
|
||||
const ollamaInstalled = !!Bun.env.PI_LOCAL_LLM && !!$which("ollama");
|
||||
|
||||
describe.skipIf(!ollamaInstalled)("Ollama Provider (gpt-oss-20b via OpenAI Completions)", () => {
|
||||
let llm: Model<"openai-completions"> | undefined;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles `omp setup <component>` to install dependencies for optional features.
|
||||
*/
|
||||
import * as path from "node:path";
|
||||
import { APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils";
|
||||
import { $which, APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { theme } from "../modes/theme/theme";
|
||||
@@ -90,12 +90,12 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
|
||||
managedEnvPath: MANAGED_PYTHON_ENV,
|
||||
};
|
||||
|
||||
const systemPythonPath = Bun.which("python") ?? Bun.which("python3");
|
||||
const systemPythonPath = $which("python") ?? $which("python3");
|
||||
const managedPath = managedPythonPath();
|
||||
const hasManagedEnv = await Bun.file(managedPath).exists();
|
||||
|
||||
result.uvPath = Bun.which("uv") ?? undefined;
|
||||
result.pipPath = Bun.which("pip3") ?? Bun.which("pip") ?? undefined;
|
||||
result.uvPath = $which("uv") ?? undefined;
|
||||
result.pipPath = $which("pip3") ?? $which("pip") ?? undefined;
|
||||
|
||||
const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
|
||||
(candidate): candidate is string => !!candidate,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
|
||||
import { $which, APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { theme } from "../modes/theme/theme";
|
||||
@@ -36,7 +36,7 @@ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolea
|
||||
}
|
||||
|
||||
async function getBunGlobalBinDir(): Promise<string | undefined> {
|
||||
if (!Bun.which("bun")) return undefined;
|
||||
if (!$which("bun")) return undefined;
|
||||
try {
|
||||
const result = await $`bun pm bin -g`.quiet().nothrow();
|
||||
if (result.exitCode !== 0) return undefined;
|
||||
@@ -167,7 +167,7 @@ function getBinaryName(): string {
|
||||
* Resolve the path that `omp` maps to in the user's PATH.
|
||||
*/
|
||||
function resolveOmpPath(): string | undefined {
|
||||
return Bun.which(APP_NAME) ?? undefined;
|
||||
return $which(APP_NAME) ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { $which } from "@oh-my-pi/pi-utils";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import type { DoctorCheck } from "./types";
|
||||
|
||||
@@ -12,7 +13,7 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
||||
];
|
||||
|
||||
for (const tool of tools) {
|
||||
const path = Bun.which(tool.name);
|
||||
const path = $which(tool.name);
|
||||
checks.push({
|
||||
name: tool.name,
|
||||
status: path ? "ok" : "warning",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { $env, getPythonEnvDir } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $which, getPythonEnvDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const DEFAULT_ENV_ALLOWLIST = new Set([
|
||||
"PATH",
|
||||
@@ -210,7 +210,7 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
|
||||
};
|
||||
}
|
||||
|
||||
const pythonPath = Bun.which("python") ?? Bun.which("python3");
|
||||
const pythonPath = $which("python") ?? $which("python3");
|
||||
if (!pythonPath) {
|
||||
throw new Error("Python executable not found on PATH");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $env, logger } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $which, logger } from "@oh-my-pi/pi-utils";
|
||||
import { TOML } from "bun";
|
||||
|
||||
/**
|
||||
@@ -141,7 +141,7 @@ export async function detectLspmux(): Promise<LspmuxState> {
|
||||
return cachedState;
|
||||
}
|
||||
|
||||
const binaryPath = Bun.which("lspmux");
|
||||
const binaryPath = $which("lspmux");
|
||||
if (!binaryPath) {
|
||||
cachedState = { available: false, running: false, binaryPath: null, config: null };
|
||||
cacheTimestamp = now;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
||||
import { $which, getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import { buildSshTarget, sanitizeHostName } from "./utils";
|
||||
|
||||
@@ -106,7 +106,7 @@ async function runSshCaptureSync(args: string[]): Promise<{ exitCode: number | n
|
||||
}
|
||||
|
||||
function ensureSshBinary(): void {
|
||||
if (!Bun.which("ssh")) {
|
||||
if (!$which("ssh")) {
|
||||
throw new Error("ssh binary not found on PATH");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
|
||||
import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
|
||||
import { buildSshTarget, sanitizeHostName } from "./utils";
|
||||
@@ -60,24 +60,24 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
|
||||
}
|
||||
|
||||
async function unmountPath(path: string): Promise<boolean> {
|
||||
const fusermount = Bun.which("fusermount") ?? Bun.which("fusermount3");
|
||||
const fusermount = $which("fusermount") ?? $which("fusermount3");
|
||||
if (fusermount) {
|
||||
const result = await $`${fusermount} -u ${path}`.quiet().nothrow();
|
||||
if (result.exitCode === 0) return true;
|
||||
}
|
||||
|
||||
const umount = Bun.which("umount");
|
||||
const umount = $which("umount");
|
||||
if (!umount) return false;
|
||||
const result = await $`${umount} ${path}`.quiet().nothrow();
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
export function hasSshfs(): boolean {
|
||||
return Bun.which("sshfs") !== null;
|
||||
return $which("sshfs") !== null;
|
||||
}
|
||||
|
||||
export async function isMounted(path: string): Promise<boolean> {
|
||||
const mountpoint = Bun.which("mountpoint");
|
||||
const mountpoint = $which("mountpoint");
|
||||
if (!mountpoint) return false;
|
||||
const result = await $`${mountpoint} -q ${path}`.quiet().nothrow();
|
||||
return result.exitCode === 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import { $which, logger } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import { resolvePython } from "./transcriber";
|
||||
|
||||
@@ -15,9 +15,9 @@ export interface EnsureOptions {
|
||||
// ── Recording tool ─────────────────────────────────────────────────
|
||||
|
||||
async function ensureRecordingTool(options?: EnsureOptions): Promise<void> {
|
||||
if (Bun.which("sox")) return;
|
||||
if (Bun.which("ffmpeg")) return;
|
||||
if (process.platform === "linux" && Bun.which("arecord")) return;
|
||||
if ($which("sox")) return;
|
||||
if ($which("ffmpeg")) return;
|
||||
if (process.platform === "linux" && $which("arecord")) return;
|
||||
|
||||
// Windows: PowerShell mciSendString is always available as fallback
|
||||
if (process.platform === "win32") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $which, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
|
||||
export interface RecordingHandle {
|
||||
@@ -15,9 +15,9 @@ const isWindows = process.platform === "win32";
|
||||
*/
|
||||
export function detectRecordingTools(): string[] {
|
||||
const tools: string[] = [];
|
||||
if (Bun.which("sox")) tools.push("sox");
|
||||
if (Bun.which("ffmpeg")) tools.push("ffmpeg");
|
||||
if (!isWindows && Bun.which("arecord")) tools.push("arecord");
|
||||
if ($which("sox")) tools.push("sox");
|
||||
if ($which("ffmpeg")) tools.push("ffmpeg");
|
||||
if (!isWindows && $which("arecord")) tools.push("arecord");
|
||||
if (isWindows) tools.push("powershell");
|
||||
return tools;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import { $which, logger } from "@oh-my-pi/pi-utils";
|
||||
import transcribeScript from "./transcribe.py" with { type: "text" };
|
||||
|
||||
export interface TranscribeOptions {
|
||||
@@ -14,7 +14,7 @@ const TRANSCRIBE_TIMEOUT_MS = 120_000;
|
||||
*/
|
||||
export function resolvePython(): string | null {
|
||||
for (const cmd of ["python", "py", "python3"]) {
|
||||
if (Bun.which(cmd)) return cmd;
|
||||
if ($which(cmd)) return cmd;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { projfsOverlayStart, projfsOverlayStop } from "@oh-my-pi/pi-natives";
|
||||
import { getWorktreeDir, isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $which, getWorktreeDir, isEnoent, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import * as git from "../utils/git";
|
||||
|
||||
@@ -348,7 +348,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
|
||||
const mergedDir = path.join(baseDir, "merged");
|
||||
|
||||
// Clean up any stale mount at this path (linux only)
|
||||
const fusermount = Bun.which("fusermount3") ?? Bun.which("fusermount");
|
||||
const fusermount = $which("fusermount3") ?? $which("fusermount");
|
||||
if (fusermount) {
|
||||
await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
|
||||
}
|
||||
@@ -358,7 +358,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
|
||||
await fs.mkdir(workDir, { recursive: true });
|
||||
await fs.mkdir(mergedDir, { recursive: true });
|
||||
|
||||
const binary = Bun.which("fuse-overlayfs");
|
||||
const binary = $which("fuse-overlayfs");
|
||||
if (!binary) {
|
||||
await fs.rm(baseDir, { recursive: true, force: true });
|
||||
throw new Error(
|
||||
@@ -380,7 +380,7 @@ export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<st
|
||||
|
||||
export async function cleanupFuseOverlay(mergedDir: string): Promise<void> {
|
||||
try {
|
||||
const fusermount = Bun.which("fusermount3") ?? Bun.which("fusermount");
|
||||
const fusermount = $which("fusermount3") ?? $which("fusermount");
|
||||
if (fusermount) {
|
||||
await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
||||
import { Readability } from "@mozilla/readability";
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import { StringEnum } from "@oh-my-pi/pi-ai";
|
||||
import { getPuppeteerDir, logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { $which, getPuppeteerDir, logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { type HTMLElement, parseHTML } from "linkedom";
|
||||
import type {
|
||||
@@ -88,8 +88,8 @@ function resolveSystemChromium(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const candidates = [
|
||||
Bun.which("chromium"),
|
||||
Bun.which("chromium-browser"),
|
||||
$which("chromium"),
|
||||
$which("chromium-browser"),
|
||||
path.join(os.homedir(), ".nix-profile/bin/chromium"),
|
||||
"/run/current-system/sw/bin/chromium",
|
||||
];
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
||||
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
||||
import { ptree, truncate } from "@oh-my-pi/pi-utils";
|
||||
import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
||||
import { parseHTML } from "linkedom";
|
||||
import type { Settings } from "../config/settings";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
@@ -94,7 +94,7 @@ const MAX_INLINE_IMAGE_OUTPUT_BYTES = 0.75 * 1024 * 1024;
|
||||
* Check if a command exists (cross-platform)
|
||||
*/
|
||||
function hasCommand(cmd: string): boolean {
|
||||
return Boolean(Bun.which(cmd));
|
||||
return Boolean($which(cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $which, isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import {
|
||||
parseDiffHunks as parseCommitDiffHunks,
|
||||
parseFileDiffs,
|
||||
@@ -162,7 +162,7 @@ function normalizeStdin(input: CommandOptions["stdin"]): "ignore" | Uint8Array {
|
||||
}
|
||||
|
||||
function ensureAvailable(): void {
|
||||
if (!Bun.which("git")) {
|
||||
if (!$which("git")) {
|
||||
throw new Error("git is not installed.");
|
||||
}
|
||||
}
|
||||
@@ -1334,13 +1334,13 @@ function formatGhFailure(args: readonly string[], stdout: string, stderr: string
|
||||
export const github = {
|
||||
/** Check if `gh` CLI is installed. */
|
||||
available(): boolean {
|
||||
return Boolean(Bun.which("gh"));
|
||||
return Boolean($which("gh"));
|
||||
},
|
||||
|
||||
/** Run a raw `gh` CLI command. Does not throw on non-zero exit. */
|
||||
async run(cwd: string, args: string[], signal?: AbortSignal, options?: GhCommandOptions): Promise<GhCommandResult> {
|
||||
throwIfAborted(signal);
|
||||
if (!Bun.which("gh")) {
|
||||
if (!$which("gh")) {
|
||||
throw new ToolError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/.");
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { APP_NAME, getToolsDir, logger, ptree, TempDir } from "@oh-my-pi/pi-utils";
|
||||
import { $which, APP_NAME, getToolsDir, logger, ptree, TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const TOOLS_DIR = getToolsDir();
|
||||
const TOOL_DOWNLOAD_TIMEOUT_MS = 120_000;
|
||||
@@ -96,7 +96,7 @@ export function getToolPath(tool: ToolName): string | null {
|
||||
// Check Python tools first
|
||||
const pythonConfig = PYTHON_TOOLS[tool];
|
||||
if (pythonConfig) {
|
||||
return Bun.which(pythonConfig.binaryName);
|
||||
return $which(pythonConfig.binaryName);
|
||||
}
|
||||
|
||||
const config = TOOLS[tool];
|
||||
@@ -109,7 +109,7 @@ export function getToolPath(tool: ToolName): string | null {
|
||||
}
|
||||
|
||||
// Check system PATH
|
||||
return Bun.which(config.binaryName);
|
||||
return $which(config.binaryName);
|
||||
}
|
||||
|
||||
// Fetch latest release version from GitHub
|
||||
@@ -249,7 +249,7 @@ async function downloadTool(tool: ToolName, signal?: AbortSignal): Promise<strin
|
||||
// Install a Python package via uv (preferred) or pip
|
||||
async function installPythonPackage(pkg: string, signal?: AbortSignal): Promise<boolean> {
|
||||
// Try uv first (faster, better isolation)
|
||||
const uv = Bun.which("uv");
|
||||
const uv = $which("uv");
|
||||
if (uv) {
|
||||
const result = await ptree.exec(["uv", "tool", "install", pkg], {
|
||||
signal,
|
||||
@@ -261,7 +261,7 @@ async function installPythonPackage(pkg: string, signal?: AbortSignal): Promise<
|
||||
}
|
||||
|
||||
// Fall back to pip
|
||||
const pip = Bun.which("pip3") || Bun.which("pip");
|
||||
const pip = $which("pip3") || $which("pip");
|
||||
if (pip) {
|
||||
const result = await ptree.exec(["pip", "install", "--user", pkg], {
|
||||
signal,
|
||||
@@ -316,7 +316,7 @@ export async function ensureTool(tool: ToolName, silentOrOptions?: EnsureToolOpt
|
||||
const success = await installPythonPackage(pythonConfig.package, signal);
|
||||
if (success) {
|
||||
// Re-check for the command after installation
|
||||
const path = Bun.which(pythonConfig.binaryName);
|
||||
const path = $which(pythonConfig.binaryName);
|
||||
if (path) {
|
||||
if (!silent) {
|
||||
logger.debug(`${pythonConfig.name} installed successfully`);
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { resetPreludeDocsCache, warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/ipy/executor";
|
||||
import { getPythonToolDescription, PythonTool } from "@oh-my-pi/pi-coding-agent/tools/python";
|
||||
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { $which, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const resolvePythonPath = (): string | null => {
|
||||
const venvPath = Bun.env.VIRTUAL_ENV;
|
||||
@@ -19,7 +19,7 @@ const resolvePythonPath = (): string | null => {
|
||||
return pythonCandidate;
|
||||
}
|
||||
}
|
||||
return Bun.which("python") ?? Bun.which("python3");
|
||||
return $which("python") ?? $which("python3");
|
||||
};
|
||||
|
||||
const pythonPath = resolvePythonPath();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { wrapToolWithMetaNotice } from "@oh-my-pi/pi-coding-agent/tools/output-m
|
||||
import { ReadTool } from "@oh-my-pi/pi-coding-agent/tools/read";
|
||||
import { WriteTool } from "@oh-my-pi/pi-coding-agent/tools/write";
|
||||
import * as markitUtils from "@oh-my-pi/pi-coding-agent/utils/markit";
|
||||
import { Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { $which, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import { unzipSync } from "fflate";
|
||||
|
||||
// Helper to extract text from content blocks
|
||||
@@ -34,7 +34,7 @@ function createFifoOrSkip(fifoPath: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mkfifoPath = Bun.which("mkfifo");
|
||||
const mkfifoPath = $which("mkfifo");
|
||||
if (!mkfifoPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import * as timers from "node:timers";
|
||||
import type { Subprocess } from "bun";
|
||||
import { $env } from "./env";
|
||||
import { $which } from "./which";
|
||||
|
||||
export interface ShellConfig {
|
||||
shell: string;
|
||||
@@ -60,18 +61,6 @@ function getShellPrefix(): string | undefined {
|
||||
return $env.PI_SHELL_PREFIX || $env.CLAUDE_CODE_SHELL_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find bash executable on PATH (Windows)
|
||||
*/
|
||||
function findBashOnPath(): string | null {
|
||||
try {
|
||||
return Bun.which("bash.exe");
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full shell config from a shell path.
|
||||
*/
|
||||
@@ -89,7 +78,7 @@ function buildConfig(shell: string): ShellConfig {
|
||||
*/
|
||||
export function resolveBasicShell(): string | undefined {
|
||||
for (const name of ["bash", "bash.exe", "sh", "sh.exe"]) {
|
||||
const resolved = Bun.which(name);
|
||||
const resolved = $which(name);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
@@ -152,7 +141,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig {
|
||||
}
|
||||
|
||||
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
||||
const bashOnPath = findBashOnPath();
|
||||
const bashOnPath = $which("bash.exe");
|
||||
if (bashOnPath) {
|
||||
cachedShellConfig = buildConfig(bashOnPath);
|
||||
return cachedShellConfig;
|
||||
|
||||
+30
-11
@@ -19,16 +19,35 @@ const MACOS_TOOL_PATHS = [
|
||||
// Map: cache key -> resolved binary path or null (not found)
|
||||
const toolCache = new Map<string | bigint, string | null>();
|
||||
|
||||
/**
|
||||
* Cache policy for which lookups.
|
||||
*/
|
||||
export const enum WhichCachePolicy {
|
||||
/**
|
||||
* Use cached result if available.
|
||||
*/
|
||||
Cached = 0,
|
||||
/**
|
||||
* Bypass cache and perform a new lookup.
|
||||
*/
|
||||
Bypass,
|
||||
/**
|
||||
* Always update cache.
|
||||
*/
|
||||
Fresh,
|
||||
/**
|
||||
* Read-only, serves from cache if present, but doesn't write.
|
||||
*/
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
// Extension: additional cache policy for tool path lookup
|
||||
export interface WhichOptions extends Bun.WhichOptions {
|
||||
/** Controls cache usage.
|
||||
*
|
||||
* - "none": disables cache, always perform a new lookup
|
||||
* - "fresh": always update cache (default)
|
||||
* - "ro": read-only, serves from cache if present, but doesn't write
|
||||
* - "cached": prefers cache if present, otherwise lookup and populate
|
||||
/**
|
||||
* Cache policy for the lookup.
|
||||
* Defaults to `WhichCachePolicy.Fresh`.
|
||||
*/
|
||||
cache?: "none" | "fresh" | "ro" | "cached";
|
||||
cache?: WhichCachePolicy;
|
||||
}
|
||||
|
||||
// Darwin-specific "which" shim: consult extra Xcode locations, then fallback to xcrun
|
||||
@@ -73,19 +92,19 @@ function cacheKey(command: string, options?: Bun.WhichOptions): string | bigint
|
||||
* @returns Filesystem path if found, else null
|
||||
*/
|
||||
export function $which(command: string, options?: WhichOptions): string | null {
|
||||
const cachePolicy = options?.cache ?? "fresh";
|
||||
const cachePolicy = options?.cache ?? WhichCachePolicy.Cached;
|
||||
let key: string | bigint | undefined;
|
||||
|
||||
if (cachePolicy !== "none") {
|
||||
if (cachePolicy !== WhichCachePolicy.Bypass) {
|
||||
key = cacheKey(command, options);
|
||||
if (cachePolicy !== "fresh") {
|
||||
if (cachePolicy !== WhichCachePolicy.Fresh) {
|
||||
const cached = toolCache.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
}
|
||||
}
|
||||
|
||||
const result = whichFresh(command, options);
|
||||
if (key != null && cachePolicy !== "ro") {
|
||||
if (key != null && cachePolicy !== WhichCachePolicy.ReadOnly) {
|
||||
toolCache.set(key, result);
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user