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:
can1357
2026-04-08 05:22:27 +02:00
parent 528d8225b7
commit 6d07944654
23 changed files with 93 additions and 81 deletions
+1 -1
View File
@@ -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` |
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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 });
+2 -1
View File
@@ -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 -4
View File
@@ -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,
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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");
}
+2 -2
View File
@@ -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");
}
}
+5 -5
View File
@@ -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;
+4 -4
View File
@@ -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") {
+4 -4
View File
@@ -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;
}
+2 -2
View File
@@ -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;
}
+4 -4
View File
@@ -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();
}
+3 -3
View File
@@ -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",
];
+2 -2
View File
@@ -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));
}
/**
+4 -4
View File
@@ -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();
+2 -2
View File
@@ -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 -14
View File
@@ -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
View File
@@ -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;