Files
openclaw/extensions/linux-node/src/executables.ts
Peter Steinberger 92cca9343e feat(linux): headless node device capabilities (camera, location, notifications) (#107193)
* feat(linux): add node device capabilities

* fix(linux-node): actionable pending-approval error + node-host advertise integration test

* fix(linux-node): map geoclue access-denied to LOCATION_DISABLED; floor camera maxWidth to avoid zero-height scale

* fix(linux-node): clamp small camera maxWidth to 2 instead of default

* docs(linux-node): clarify where-am-i -t is a process timeout, not update throttle

* refactor(gateway): extract legacy-node filter + rejection hint to fit LOC ratchet; docs-map + deadcode baseline

* fix(gateway): drop now-unused DEFAULT_DANGEROUS_NODE_COMMANDS import after hint extraction

* test(node-host): drop imports orphaned by removed error-code test
2026-07-14 02:30:36 -07:00

42 lines
1.1 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
export type ExecutableResolver = (
command: string,
env: NodeJS.ProcessEnv,
extraCandidates?: readonly string[],
) => string | null;
export function createCachedExecutableResolver(
isExecutable: (candidate: string) => boolean = (candidate) => {
try {
fs.accessSync(candidate, fs.constants.X_OK);
return true;
} catch {
return false;
}
},
): ExecutableResolver {
const cache = new Map<string, string | null>();
return (command, env, extraCandidates = []) => {
const pathValue = env.PATH ?? "";
const key = `${command}\0${pathValue}\0${extraCandidates.join("\0")}`;
if (cache.has(key)) {
return cache.get(key) ?? null;
}
const pathCandidates = pathValue
.split(path.delimiter)
.filter(Boolean)
.map((dir) => path.join(dir, command));
const candidates = path.isAbsolute(command)
? [command]
: [...pathCandidates, ...extraCandidates];
const found = candidates.find(isExecutable) ?? null;
cache.set(key, found);
return found;
};
}
export const resolveExecutable = createCachedExecutableResolver();