mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 10:50:58 +00:00
283 lines
7.8 KiB
TypeScript
283 lines
7.8 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
|
|
import { getPrimaryCommand } from "./argv.js";
|
|
import { takeCliRootOptionValue } from "./root-option-value.js";
|
|
|
|
type CliContainerParseResult =
|
|
| { ok: true; container: string | null; argv: string[] }
|
|
| { ok: false; error: string };
|
|
|
|
export type CliContainerTargetResult =
|
|
| { handled: true; exitCode: number }
|
|
| { handled: false; argv: string[] };
|
|
|
|
type ContainerTargetDeps = {
|
|
env: NodeJS.ProcessEnv;
|
|
spawnSync: typeof spawnSync;
|
|
stdinIsTTY: boolean;
|
|
stdoutIsTTY: boolean;
|
|
};
|
|
|
|
type ContainerRuntimeExec = {
|
|
runtime: "podman" | "docker";
|
|
command: string;
|
|
argsPrefix: string[];
|
|
};
|
|
|
|
export function parseCliContainerArgs(argv: string[]): CliContainerParseResult {
|
|
if (argv.length < 2) {
|
|
return { ok: true, container: null, argv };
|
|
}
|
|
|
|
const out: string[] = argv.slice(0, 2);
|
|
let container: string | null = null;
|
|
|
|
const args = argv.slice(2);
|
|
for (let i = 0; i < args.length; i += 1) {
|
|
const arg = args[i];
|
|
if (arg === undefined) {
|
|
continue;
|
|
}
|
|
if (arg === FLAG_TERMINATOR) {
|
|
out.push(arg, ...args.slice(i + 1));
|
|
break;
|
|
}
|
|
|
|
if (arg === "--container" || arg.startsWith("--container=")) {
|
|
const next = args[i + 1];
|
|
const { value, consumedNext } = takeCliRootOptionValue(arg, next);
|
|
if (consumedNext) {
|
|
i += 1;
|
|
}
|
|
if (!value) {
|
|
return { ok: false, error: "--container requires a value" };
|
|
}
|
|
container = value;
|
|
continue;
|
|
}
|
|
|
|
const consumedRootOption = consumeRootOptionToken(args, i);
|
|
if (consumedRootOption > 0) {
|
|
for (let offset = 0; offset < consumedRootOption; offset += 1) {
|
|
const token = args[i + offset];
|
|
if (token !== undefined) {
|
|
out.push(token);
|
|
}
|
|
}
|
|
i += consumedRootOption - 1;
|
|
continue;
|
|
}
|
|
|
|
out.push(arg);
|
|
}
|
|
|
|
return { ok: true, container, argv: out };
|
|
}
|
|
|
|
export function resolveCliContainerTarget(
|
|
argv: string[],
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): string | null {
|
|
const parsed = parseCliContainerArgs(argv);
|
|
if (!parsed.ok) {
|
|
throw new Error(parsed.error);
|
|
}
|
|
return parsed.container ?? env.OPENCLAW_CONTAINER?.trim() ?? null;
|
|
}
|
|
|
|
function isContainerRunning(params: {
|
|
exec: ContainerRuntimeExec;
|
|
containerName: string;
|
|
deps: Pick<ContainerTargetDeps, "spawnSync">;
|
|
}): boolean {
|
|
const result = params.deps.spawnSync(
|
|
params.exec.command,
|
|
[...params.exec.argsPrefix, "inspect", "--format", "{{.State.Running}}", params.containerName],
|
|
params.exec.command === "sudo"
|
|
? { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] }
|
|
: { encoding: "utf8" },
|
|
);
|
|
return result.status === 0 && result.stdout.trim() === "true";
|
|
}
|
|
|
|
function candidateContainerRuntimes(env: NodeJS.ProcessEnv): ContainerRuntimeExec[] {
|
|
const candidates: ContainerRuntimeExec[] = [
|
|
{
|
|
runtime: "podman",
|
|
command: "podman",
|
|
argsPrefix: [],
|
|
},
|
|
{
|
|
runtime: "docker",
|
|
command: "docker",
|
|
argsPrefix: [],
|
|
},
|
|
];
|
|
const podmanUser = env.OPENCLAW_PODMAN_USER?.trim() || "openclaw";
|
|
const currentUser = env.USER?.trim() || env.LOGNAME?.trim() || "";
|
|
if (podmanUser && currentUser && podmanUser !== currentUser) {
|
|
candidates.push({
|
|
runtime: "podman",
|
|
command: "sudo",
|
|
argsPrefix: ["-u", podmanUser, "podman"],
|
|
});
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function describeContainerRuntimeExec(exec: ContainerRuntimeExec): string {
|
|
if (exec.command === "sudo") {
|
|
const podmanUser = exec.argsPrefix[1];
|
|
return `podman (via sudo -u ${podmanUser})`;
|
|
}
|
|
return exec.runtime;
|
|
}
|
|
|
|
function resolveRunningContainer(params: {
|
|
containerName: string;
|
|
env: NodeJS.ProcessEnv;
|
|
deps: Pick<ContainerTargetDeps, "spawnSync">;
|
|
}): (ContainerRuntimeExec & { containerName: string }) | null {
|
|
const matches: Array<ContainerRuntimeExec & { containerName: string }> = [];
|
|
const candidates = candidateContainerRuntimes(params.env);
|
|
for (const exec of candidates) {
|
|
if (
|
|
isContainerRunning({
|
|
exec,
|
|
containerName: params.containerName,
|
|
deps: params.deps,
|
|
})
|
|
) {
|
|
matches.push({ ...exec, containerName: params.containerName });
|
|
if (exec.runtime === "docker") {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (matches.length === 0) {
|
|
return null;
|
|
}
|
|
if (matches.length > 1) {
|
|
const runtimes = matches.map(describeContainerRuntimeExec).join(", ");
|
|
throw new Error(
|
|
`Container "${params.containerName}" is running under multiple runtimes (${runtimes}); use a unique container name.`,
|
|
);
|
|
}
|
|
return matches[0];
|
|
}
|
|
|
|
function buildContainerExecArgs(params: {
|
|
exec: ContainerRuntimeExec;
|
|
containerName: string;
|
|
argv: string[];
|
|
stdinIsTTY: boolean;
|
|
stdoutIsTTY: boolean;
|
|
}): string[] {
|
|
const envFlag = params.exec.runtime === "docker" ? "-e" : "--env";
|
|
const interactiveFlags = ["-i", ...(params.stdinIsTTY && params.stdoutIsTTY ? ["-t"] : [])];
|
|
return [
|
|
...params.exec.argsPrefix,
|
|
"exec",
|
|
...interactiveFlags,
|
|
envFlag,
|
|
`OPENCLAW_CONTAINER_HINT=${params.containerName}`,
|
|
envFlag,
|
|
"OPENCLAW_CLI_CONTAINER_BYPASS=1",
|
|
params.containerName,
|
|
"openclaw",
|
|
...params.argv,
|
|
];
|
|
}
|
|
|
|
function buildContainerExecEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
return {
|
|
...env,
|
|
// The child CLI should render container-aware follow-up commands via
|
|
// OPENCLAW_CONTAINER_HINT, but it should not treat itself as still
|
|
// container-targeted for validation/routing.
|
|
OPENCLAW_CONTAINER: "",
|
|
};
|
|
}
|
|
|
|
function isBlockedContainerCommand(argv: string[]): boolean {
|
|
if (getPrimaryCommand(["node", "openclaw", ...argv]) === "update") {
|
|
return true;
|
|
}
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (!arg || arg === FLAG_TERMINATOR) {
|
|
return false;
|
|
}
|
|
if (arg === "--update") {
|
|
return true;
|
|
}
|
|
const consumedRootOption = consumeRootOptionToken(argv, i);
|
|
if (consumedRootOption > 0) {
|
|
i += consumedRootOption - 1;
|
|
continue;
|
|
}
|
|
if (!arg.startsWith("-")) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function maybeRunCliInContainer(
|
|
argv: string[],
|
|
deps?: Partial<ContainerTargetDeps>,
|
|
): CliContainerTargetResult {
|
|
const resolvedDeps: ContainerTargetDeps = {
|
|
env: deps?.env ?? process.env,
|
|
spawnSync: deps?.spawnSync ?? spawnSync,
|
|
stdinIsTTY: deps?.stdinIsTTY ?? Boolean(process.stdin.isTTY),
|
|
stdoutIsTTY: deps?.stdoutIsTTY ?? Boolean(process.stdout.isTTY),
|
|
};
|
|
|
|
if (resolvedDeps.env.OPENCLAW_CLI_CONTAINER_BYPASS === "1") {
|
|
return { handled: false, argv };
|
|
}
|
|
|
|
const parsed = parseCliContainerArgs(argv);
|
|
if (!parsed.ok) {
|
|
throw new Error(parsed.error);
|
|
}
|
|
const containerName = resolveCliContainerTarget(argv, resolvedDeps.env);
|
|
if (!containerName) {
|
|
return { handled: false, argv: parsed.argv };
|
|
}
|
|
if (isBlockedContainerCommand(parsed.argv.slice(2))) {
|
|
throw new Error(
|
|
"openclaw update is not supported with --container; rebuild or restart the container image instead.",
|
|
);
|
|
}
|
|
|
|
const runningContainer = resolveRunningContainer({
|
|
containerName,
|
|
env: resolvedDeps.env,
|
|
deps: resolvedDeps,
|
|
});
|
|
if (!runningContainer) {
|
|
throw new Error(`No running container matched "${containerName}" under podman or docker.`);
|
|
}
|
|
|
|
const result = resolvedDeps.spawnSync(
|
|
runningContainer.command,
|
|
buildContainerExecArgs({
|
|
exec: runningContainer,
|
|
containerName: runningContainer.containerName,
|
|
argv: parsed.argv.slice(2),
|
|
stdinIsTTY: resolvedDeps.stdinIsTTY,
|
|
stdoutIsTTY: resolvedDeps.stdoutIsTTY,
|
|
}),
|
|
{
|
|
stdio: "inherit",
|
|
env: buildContainerExecEnv(resolvedDeps.env),
|
|
},
|
|
);
|
|
return {
|
|
handled: true,
|
|
exitCode: typeof result.status === "number" ? result.status : 1,
|
|
};
|
|
}
|