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);
}

View File

@ -811,6 +811,9 @@
"optional": true
}
},
"optionalDependencies": {
"openshell": "0.1.0"
},
"engines": {
"node": ">=22.16.0"
},

View File

@ -234,6 +234,10 @@ importers:
vitest:
specifier: ^4.1.0
version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.0)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
optionalDependencies:
openshell:
specifier: 0.1.0
version: 0.1.0
extensions/acpx:
dependencies:
@ -3268,6 +3272,9 @@ packages:
'@swc/helpers@0.5.19':
resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==}
'@telegraf/types@7.1.0':
resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==}
'@thi.ng/bitstream@2.4.44':
resolution: {integrity: sha512-SN5GtdycUC8J9kRn4+GAcAJ93F9vKtaiI/SjpOyLl911bWNlF8gANFFCdgBkzbF2DHcpKflHxrVVfrYB6aCdsw==}
engines: {node: '>=18'}
@ -3859,12 +3866,21 @@ packages:
bs58@6.0.0:
resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
buffer-alloc-unsafe@1.1.0:
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
buffer-alloc@1.2.0:
resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-fill@1.0.0:
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@ -4160,6 +4176,10 @@ packages:
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dotenv@16.6.1:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
dotenv@17.3.1:
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
engines: {node: '>=12'}
@ -5187,6 +5207,10 @@ packages:
mpg123-decoder@1.0.3:
resolution: {integrity: sha512-+fjxnWigodWJm3+4pndi+KUg9TBojgn31DPk85zEsim7C6s0X5Ztc/hQYdytXkwuGXH+aB0/aEkG40Emukv6oQ==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@ -5374,6 +5398,11 @@ packages:
zod:
optional: true
openshell@0.1.0:
resolution: {integrity: sha512-B7jLewH+d73hraWcrSFgNOjvd+frW5JPejkTpqgj2EJBjX/Yk1Y4blgP5pDl4FwrBxfmwsTKR08Uwgrdo+xpSg==}
engines: {node: '>=18'}
hasBin: true
opus-decoder@0.7.11:
resolution: {integrity: sha512-+e+Jz3vGQLxRTBHs8YJQPRPc1Tr+/aC6coV/DlZylriA29BdHQAYXhvNRKtjftof17OFng0+P4wsFIqQu3a48A==}
@ -5431,6 +5460,10 @@ packages:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
p-timeout@4.1.0:
resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==}
engines: {node: '>=10'}
p-timeout@7.0.1:
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
engines: {node: '>=20'}
@ -5849,6 +5882,9 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-compare@1.1.4:
resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
@ -5856,6 +5892,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sandwich-stream@2.0.2:
resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==}
engines: {node: '>= 0.10'}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
@ -6144,6 +6184,11 @@ packages:
teex@1.0.1:
resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==}
telegraf@4.16.3:
resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==}
engines: {node: ^12.20.0 || >=14.13.1}
hasBin: true
text-decoder@1.2.7:
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
@ -9743,6 +9788,9 @@ snapshots:
dependencies:
tslib: 2.8.1
'@telegraf/types@7.1.0':
optional: true
'@thi.ng/bitstream@2.4.44':
dependencies:
'@thi.ng/errors': 2.6.6
@ -10406,10 +10454,22 @@ snapshots:
dependencies:
base-x: 5.0.1
buffer-alloc-unsafe@1.1.0:
optional: true
buffer-alloc@1.2.0:
dependencies:
buffer-alloc-unsafe: 1.1.0
buffer-fill: 1.0.0
optional: true
buffer-crc32@0.2.13: {}
buffer-equal-constant-time@1.0.1: {}
buffer-fill@1.0.0:
optional: true
buffer-from@1.1.2: {}
bun-types@1.3.9:
@ -10689,6 +10749,9 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
dotenv@16.6.1:
optional: true
dotenv@17.3.1: {}
dts-resolver@2.1.3: {}
@ -11872,6 +11935,9 @@ snapshots:
'@wasm-audio-decoders/common': 9.0.7
optional: true
mri@1.2.0:
optional: true
mrmime@2.0.1: {}
ms@2.1.3: {}
@ -12099,6 +12165,15 @@ snapshots:
ws: 8.20.0
zod: 4.3.6
openshell@0.1.0:
dependencies:
dotenv: 16.6.1
telegraf: 4.16.3
transitivePeerDependencies:
- encoding
- supports-color
optional: true
opus-decoder@0.7.11:
dependencies:
'@wasm-audio-decoders/common': 9.0.7
@ -12200,6 +12275,9 @@ snapshots:
dependencies:
p-finally: 1.0.0
p-timeout@4.1.0:
optional: true
p-timeout@7.0.1: {}
pac-proxy-agent@7.2.0:
@ -12693,10 +12771,18 @@ snapshots:
safe-buffer@5.2.1: {}
safe-compare@1.1.4:
dependencies:
buffer-alloc: 1.2.0
optional: true
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {}
sandwich-stream@2.0.2:
optional: true
sax@1.6.0:
optional: true
@ -13053,6 +13139,21 @@ snapshots:
- bare-abort-controller
- react-native-b4a
telegraf@4.16.3:
dependencies:
'@telegraf/types': 7.1.0
abort-controller: 3.0.0
debug: 4.4.3
mri: 1.2.0
node-fetch: 2.7.0
p-timeout: 4.1.0
safe-compare: 1.1.4
sandwich-stream: 2.0.2
transitivePeerDependencies:
- encoding
- supports-color
optional: true
text-decoder@1.2.7:
dependencies:
b4a: 1.8.0