fix(openshell): bundle upstream cli fallback

This commit is contained in:
Vincent Koc
2026-03-22 19:18:51 -07:00
parent f8731b3d9d
commit 009980465f
4 changed files with 174 additions and 3 deletions

View File

@@ -1,8 +1,18 @@
import { describe, expect, it } from "vitest";
import { buildExecRemoteCommand, buildOpenShellBaseArgv, shellEscape } from "./cli.js";
import { afterEach, describe, expect, it } from "vitest";
import {
buildExecRemoteCommand,
buildOpenShellBaseArgv,
resolveOpenShellCommand,
setBundledOpenShellCommandResolverForTest,
shellEscape,
} from "./cli.js";
import { resolveOpenShellPluginConfig } from "./config.js";
describe("openshell cli helpers", () => {
afterEach(() => {
setBundledOpenShellCommandResolverForTest();
});
it("builds base argv with gateway overrides", () => {
const config = resolveOpenShellPluginConfig({
command: "/usr/local/bin/openshell",
@@ -18,6 +28,20 @@ describe("openshell cli helpers", () => {
]);
});
it("prefers the bundled openshell command when available", () => {
setBundledOpenShellCommandResolverForTest(() => "/tmp/node_modules/.bin/openshell");
const config = resolveOpenShellPluginConfig(undefined);
expect(resolveOpenShellCommand("openshell")).toBe("/tmp/node_modules/.bin/openshell");
expect(buildOpenShellBaseArgv(config)).toEqual(["/tmp/node_modules/.bin/openshell"]);
});
it("falls back to the PATH command when no bundled openshell is present", () => {
setBundledOpenShellCommandResolverForTest(() => null);
expect(resolveOpenShellCommand("openshell")).toBe("openshell");
});
it("shell escapes single quotes", () => {
expect(shellEscape(`a'b`)).toBe(`'a'"'"'b'`);
});

View File

@@ -1,3 +1,6 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import {
buildExecRemoteCommand,
createSshSandboxSessionFromConfigText,
@@ -9,14 +12,54 @@ import type { ResolvedOpenShellPluginConfig } from "./config.js";
export { buildExecRemoteCommand, shellEscape } from "openclaw/plugin-sdk/sandbox";
const require = createRequire(import.meta.url);
let cachedBundledOpenShellCommand: string | null | undefined;
let bundledCommandResolverForTest: (() => string | null) | undefined;
export type OpenShellExecContext = {
config: ResolvedOpenShellPluginConfig;
sandboxName: string;
timeoutMs?: number;
};
export function setBundledOpenShellCommandResolverForTest(resolver?: () => string | null): void {
bundledCommandResolverForTest = resolver;
cachedBundledOpenShellCommand = undefined;
}
function resolveBundledOpenShellCommand(): string | null {
if (bundledCommandResolverForTest) {
return bundledCommandResolverForTest();
}
if (cachedBundledOpenShellCommand !== undefined) {
return cachedBundledOpenShellCommand;
}
try {
const packageJsonPath = require.resolve("openshell/package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
bin?: string | Record<string, string>;
};
const relativeBin =
typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.openshell;
cachedBundledOpenShellCommand = relativeBin
? path.resolve(path.dirname(packageJsonPath), relativeBin)
: null;
} catch {
cachedBundledOpenShellCommand = null;
}
return cachedBundledOpenShellCommand;
}
export function resolveOpenShellCommand(command: string): string {
if (command !== "openshell") {
return command;
}
return resolveBundledOpenShellCommand() ?? command;
}
export function buildOpenShellBaseArgv(config: ResolvedOpenShellPluginConfig): string[] {
const argv = [config.command];
const argv = [resolveOpenShellCommand(config.command)];
if (config.gateway) {
argv.push("--gateway", config.gateway);
}