CLI: preserve profile passthrough args and env overrides

This commit is contained in:
Tristan Manchester 2026-03-01 10:28:43 +01:00
parent e13cc5b66b
commit 0a30503239
2 changed files with 117 additions and 16 deletions

View File

@ -103,6 +103,48 @@ describe("parseCliProfileArgs", () => {
]);
});
it("does not intercept --profile in nodes run argv without passthrough terminator", () => {
const res = parseCliProfileArgs([
"node",
"openclaw",
"nodes",
"run",
"--node",
"abc123",
"aws",
"--profile",
"prod",
"sts",
"get-caller-identity",
]);
if (!res.ok) {
throw new Error(res.error);
}
expect(res.profile).toBeNull();
expect(res.argv).toEqual([
"node",
"openclaw",
"nodes",
"run",
"--node",
"abc123",
"aws",
"--profile",
"prod",
"sts",
"get-caller-identity",
]);
});
it("does not intercept --profile in docs query args", () => {
const res = parseCliProfileArgs(["node", "openclaw", "docs", "aws", "--profile", "prod"]);
if (!res.ok) {
throw new Error(res.error);
}
expect(res.profile).toBeNull();
expect(res.argv).toEqual(["node", "openclaw", "docs", "aws", "--profile", "prod"]);
});
it("keeps passthrough --profile when global --profile is set before terminator", () => {
const res = parseCliProfileArgs([
"node",
@ -183,12 +225,11 @@ describe("applyCliProfileEnv", () => {
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_STATE_DIR).toBe("/custom");
// OPENCLAW_GATEWAY_PORT is intentionally reset for profile isolation.
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19001");
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19099");
expect(env.OPENCLAW_CONFIG_PATH).toBe(path.join("/custom", "openclaw.json"));
});
it("clears inherited OPENCLAW_GATEWAY_PORT for non-dev profiles", () => {
it("preserves explicit OPENCLAW_GATEWAY_PORT for non-dev profiles", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_GATEWAY_PORT: "18789",
};
@ -197,10 +238,10 @@ describe("applyCliProfileEnv", () => {
env,
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_GATEWAY_PORT).toBeUndefined();
expect(env.OPENCLAW_GATEWAY_PORT).toBe("18789");
});
it("clears inherited service env vars for profile isolation", () => {
it("preserves service override env vars", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_GATEWAY_PORT: "18789",
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway",
@ -212,13 +253,23 @@ describe("applyCliProfileEnv", () => {
env,
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_GATEWAY_PORT).toBeUndefined();
expect(env.OPENCLAW_LAUNCHD_LABEL).toBeUndefined();
expect(env.OPENCLAW_SYSTEMD_UNIT).toBeUndefined();
expect(env.OPENCLAW_SERVICE_VERSION).toBeUndefined();
expect(env.OPENCLAW_GATEWAY_PORT).toBe("18789");
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.gateway");
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-gateway.service");
expect(env.OPENCLAW_SERVICE_VERSION).toBe("2026.1.0");
expect(env.OPENCLAW_PROFILE).toBe("work");
});
it("uses 19001 for dev profile only when OPENCLAW_GATEWAY_PORT is unset", () => {
const env: Record<string, string | undefined> = {};
applyCliProfileEnv({
profile: "dev",
env,
homedir: () => "/home/peter",
});
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19001");
});
it("uses OPENCLAW_HOME when deriving profile state dir", () => {
const env: Record<string, string | undefined> = {
OPENCLAW_HOME: "/srv/openclaw-home",

View File

@ -27,6 +27,57 @@ function takeValue(
return { value: trimmed || null, consumedNext: Boolean(next) };
}
const ARBITRARY_ARG_COMMAND_PATHS = [["nodes", "run"], ["docs"]] as const;
function shouldGuardTrailingArgsFromProfileParsing(args: string[]): boolean {
const commandTokens: string[] = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
break;
}
if (arg === "--dev") {
continue;
}
if (arg === "--profile" || arg.startsWith("--profile=")) {
if (arg === "--profile") {
i += 1;
}
continue;
}
if (arg.startsWith("-")) {
continue;
}
commandTokens.push(arg);
if (
ARBITRARY_ARG_COMMAND_PATHS.some(
(path) =>
path.length === commandTokens.length &&
path.every((part, idx) => part === commandTokens[idx]),
)
) {
return true;
}
const couldStillMatch = ARBITRARY_ARG_COMMAND_PATHS.some(
(path) =>
commandTokens.length <= path.length &&
commandTokens.every((part, idx) => path[idx] === part),
);
if (!couldStillMatch) {
return false;
}
}
return false;
}
export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
if (argv.length < 2) {
return { ok: true, profile: null, argv };
@ -39,6 +90,7 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
let sawTerminator = false;
const args = argv.slice(2);
const guardTrailingArgs = shouldGuardTrailingArgsFromProfileParsing(args);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === undefined) {
@ -56,7 +108,10 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
continue;
}
if (sawCommand && arg !== "--profile" && !arg.startsWith("--profile=")) {
if (
sawCommand &&
(guardTrailingArgs || (arg !== "--profile" && !arg.startsWith("--profile=")))
) {
out.push(arg);
continue;
}
@ -147,12 +202,7 @@ export function applyCliProfileEnv(params: {
}
// Convenience only: fill defaults, never override explicit env values.
// Exception: clear inherited service-scoped env vars for profile isolation.
env.OPENCLAW_PROFILE = profile;
delete env.OPENCLAW_GATEWAY_PORT;
delete env.OPENCLAW_LAUNCHD_LABEL;
delete env.OPENCLAW_SYSTEMD_UNIT;
delete env.OPENCLAW_SERVICE_VERSION;
const stateDir = env.OPENCLAW_STATE_DIR?.trim() || resolveProfileStateDir(profile, env, homedir);
if (!env.OPENCLAW_STATE_DIR?.trim()) {
@ -163,7 +213,7 @@ export function applyCliProfileEnv(params: {
env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json");
}
if (profile === "dev") {
if (profile === "dev" && !env.OPENCLAW_GATEWAY_PORT?.trim()) {
env.OPENCLAW_GATEWAY_PORT = "19001";
}
}