openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
133 lines (132 loc) • 6.74 kB
JavaScript
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { t as expandHomePrefix } from "./home-dir-CjKH62jd.mjs";
import { r as resolveEnvironmentValue } from "./process-env-DlZFJzq6.mjs";
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.mjs";
import fs from "node:fs";
import path from "node:path";
import { safeStatSync } from "@openclaw/fs-safe/path";
//#region src/infra/executable-path.ts
function isDriveLessWindowsRootedPath(value) {
return process.platform === "win32" && /^:[\\/]/.test(value);
}
function resolveExecutablePathCandidate(rawExecutable, options) {
const expanded = rawExecutable.startsWith("~") ? expandHomePrefix(rawExecutable, { env: options?.env }) : rawExecutable;
if (isDriveLessWindowsRootedPath(expanded)) return;
const hasPathSeparator = expanded.includes("/") || expanded.includes("\\");
if (options?.requirePathSeparator && !hasPathSeparator) return;
if (!hasPathSeparator) return expanded;
if (path.isAbsolute(expanded)) return path.resolve(expanded);
const base = options?.cwd && options.cwd.trim() ? options.cwd.trim() : process.cwd();
return path.resolve(base, expanded);
}
function resolveWindowsExecutableExtensions(executable, env, includeExtensionless = true) {
if (process.platform !== "win32" || path.extname(executable).length > 0) return [""];
const extensions = [...resolveWindowsExecutableExtSet(env)];
return includeExtensionless ? ["", ...extensions] : extensions;
}
function resolveWindowsExecutableExtSet(env) {
return new Set((resolveEnvironmentValue(env, "PATHEXT") ?? resolveEnvironmentValue(process.env, "PATHEXT") ?? ".EXE;.CMD;.BAT;.COM").split(";").map((ext) => normalizeLowercaseStringOrEmpty(ext)).filter(Boolean));
}
function isRegularFile(filePath) {
return safeStatSync(filePath)?.isFile() ?? false;
}
const WINDOWS_NATIVE_EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
".com",
".exe",
".bat",
".cmd"
]);
/** Checks a supplied path without PATH lookup or lexical normalization. */
function isExecutableFile(filePath, options) {
if (!isRegularFile(filePath)) return false;
try {
if (process.platform === "win32") {
const ext = normalizeLowercaseStringOrEmpty(path.extname(filePath));
if (!ext || WINDOWS_NATIVE_EXECUTABLE_EXTENSIONS.has(ext)) return true;
return resolveWindowsExecutableExtSet(options?.env).has(ext);
}
fs.accessSync(filePath, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
const EXECUTABLE_PATH_CACHE_TTL_MS = 6e4;
const EXECUTABLE_PATH_CACHE_MAX_ENTRIES = 128;
const executablePathCache = /* @__PURE__ */ new Map();
function cacheExecutablePath(key, resolved) {
executablePathCache.set(key, {
expiresAt: Date.now() + EXECUTABLE_PATH_CACHE_TTL_MS,
resolved: resolved ?? null
});
pruneMapToMaxSize(executablePathCache, EXECUTABLE_PATH_CACHE_MAX_ENTRIES);
}
function executablePathCacheKey(executable, pathEnv, env, includeExtensionless, requestedCwd) {
const pathExt = resolveEnvironmentValue(env, "PATHEXT") ?? resolveEnvironmentValue(process.env, "PATHEXT") ?? "";
let cwd = "";
try {
cwd = requestedCwd ?? process.cwd();
} catch {}
return `${process.platform}\0${executable}\0${pathEnv}\0${pathExt}\0${includeExtensionless !== false}\0${cwd}\0${requestedCwd !== void 0}`;
}
/** Clears process-local PATH probe results after the runtime environment changes. */
function clearExecutablePathCache() {
executablePathCache.clear();
}
function resolveExecutableFromPathEnv(executable, pathEnv, env, options) {
const cwd = options?.cwd?.trim() ? path.resolve(options.cwd.trim()) : void 0;
const useCache = options?.useCache !== false;
const cacheKey = executablePathCacheKey(executable, pathEnv, env, options?.includeExtensionless, cwd);
const cached = useCache ? executablePathCache.get(cacheKey) : void 0;
const now = Date.now();
if (cached && cached.expiresAt > now) {
cached.expiresAt = now + EXECUTABLE_PATH_CACHE_TTL_MS;
executablePathCache.delete(cacheKey);
executablePathCache.set(cacheKey, cached);
return cached.resolved ?? void 0;
}
if (cached) executablePathCache.delete(cacheKey);
const delimiter = process.platform === "win32" ? ";" : path.delimiter;
const entries = pathEnv.split(delimiter).filter((entry) => Boolean(entry) || cwd !== void 0 && process.platform !== "win32");
const extensions = resolveWindowsExecutableExtensions(executable, env, options?.includeExtensionless);
for (const entry of entries) {
const hasParentTraversal = process.platform !== "win32" && entry.split("/").includes("..");
const rawDirectory = cwd !== void 0 && !path.isAbsolute(entry) ? `${cwd}${path.sep}${entry}` : entry;
for (const ext of extensions) {
const candidate = hasParentTraversal ? `${rawDirectory}${path.sep}${executable}${ext}` : path.join(cwd === void 0 ? entry : path.resolve(cwd, entry), executable + ext);
if (isExecutableFile(candidate, { env })) {
if (useCache) cacheExecutablePath(cacheKey, candidate);
return candidate;
}
}
}
if (useCache) cacheExecutablePath(cacheKey, void 0);
}
function resolveExecutablePath(rawExecutable, options) {
const candidate = resolveExecutablePathCandidate(rawExecutable, options);
if (!candidate) return;
if (candidate.includes("/") || candidate.includes("\\")) return isExecutableFile(candidate, options) ? candidate : void 0;
return resolveExecutableFromPathEnv(candidate, resolveEnvironmentValue(options?.env, "PATH") ?? resolveEnvironmentValue(process.env, "PATH") ?? "", options?.env, {
cwd: options?.cwd,
useCache: options?.useCache
});
}
/**
* On Windows, resolves a bare command name to its full .cmd or .exe path by
* probing PATH/PATHEXT without executing another resolver. On non-Windows this
* is a no-op.
*/
function resolveExecutable(cmd) {
if (process.platform !== "win32") return cmd;
if (WINDOWS_NATIVE_EXECUTABLE_EXTENSIONS.has(normalizeLowercaseStringOrEmpty(path.extname(cmd)))) return cmd;
const entries = (resolveEnvironmentValue(process.env, "PATH") ?? "").split(";").filter(Boolean);
const extensions = resolveWindowsExecutableExtensions(cmd, process.env);
const matches = [];
for (const entry of entries) for (const ext of extensions) {
const candidate = path.join(entry, cmd + ext);
if (isExecutableFile(candidate, { env: process.env })) matches.push(candidate);
}
return matches.find((match) => normalizeLowercaseStringOrEmpty(path.extname(match)) === ".cmd") ?? matches.find((match) => normalizeLowercaseStringOrEmpty(path.extname(match)) === ".exe") ?? matches[0] ?? cmd;
}
//#endregion
export { resolveExecutableFromPathEnv as a, resolveExecutable as i, isExecutableFile as n, resolveExecutablePath as o, isRegularFile as r, resolveExecutablePathCandidate as s, clearExecutablePathCache as t };