diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index 36067ab12f6..17204d89d6b 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -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 = { 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 = { 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 = {}; + 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 = { OPENCLAW_HOME: "/srv/openclaw-home", diff --git a/src/cli/profile.ts b/src/cli/profile.ts index 0ddc879675e..8b831380fc2 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -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"; } }