diff --git a/src/cli/gateway-cli/run.option-collisions.test.ts b/src/cli/gateway-cli/run.option-collisions.test.ts index 3ce8077b3d1..aaa306c160f 100644 --- a/src/cli/gateway-cli/run.option-collisions.test.ts +++ b/src/cli/gateway-cli/run.option-collisions.test.ts @@ -381,6 +381,22 @@ describe("gateway run option collisions", () => { ); }); + it("hard-stops --dev --reset when state is profile-default but config is custom", async () => { + vi.stubEnv("HOME", "/Users/test"); + vi.stubEnv("OPENCLAW_PROFILE", "work"); + vi.stubEnv("OPENCLAW_STATE_DIR", "/Users/test/.openclaw-work"); + vi.stubEnv("OPENCLAW_CONFIG_PATH", "/tmp/custom-dev/openclaw.json"); + resolveStateDir.mockReturnValue("/Users/test/.openclaw-work"); + resolveConfigPath.mockReturnValue("/tmp/custom-dev/openclaw.json"); + + await expectGatewayExit(["gateway", "run", "--dev", "--reset"]); + + expect(ensureDevGatewayConfig).not.toHaveBeenCalled(); + expect(runtimeErrors.join("\n")).toContain( + "Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.", + ); + }); + it("treats symlinked default paths as default reset targets", async () => { const home = "/Users/test"; const defaultStateDir = path.join(home, ".openclaw"); diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index 63151437d16..4f0e8a3dba3 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -246,14 +246,14 @@ async function runGatewayCommand(opts: GatewayRunOpts) { const configMatchesProfileDefault = profileDefaultPaths ? resolvedConfigPath === canonicalizePathForCompare(profileDefaultPaths.configPath) : false; - const targetMatchesProfileDefaults = stateMatchesProfileDefault && configMatchesProfileDefault; + const stateTargetsProfileDefault = stateIsDefault || stateMatchesProfileDefault; + const configTargetsProfileDefault = configIsDefault || configMatchesProfileDefault; const hasStateOverride = Boolean(process.env.OPENCLAW_STATE_DIR?.trim()); const hasConfigOverride = Boolean(process.env.OPENCLAW_CONFIG_PATH?.trim()); const hasExplicitCustomTarget = - (hasStateOverride || hasConfigOverride) && - !stateIsDefault && - !configIsDefault && - !targetMatchesProfileDefaults; + hasStateOverride && + !stateTargetsProfileDefault && + (!hasConfigOverride || !configTargetsProfileDefault); if (!hasExplicitCustomTarget && (!stateMatchesDev || !configMatchesDev)) { defaultRuntime.error( diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index 17204d89d6b..14a66fd5b72 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -176,6 +176,41 @@ describe("parseCliProfileArgs", () => { ]); }); + it("consumes root option values before command-path passthrough guard", () => { + const res = parseCliProfileArgs([ + "node", + "openclaw", + "--log-level", + "debug", + "nodes", + "run", + "--", + "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", + "--log-level", + "debug", + "nodes", + "run", + "--", + "aws", + "--profile", + "prod", + "sts", + "get-caller-identity", + ]); + }); + it("parses --profile value and strips it", () => { const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "status"]); if (!res.ok) { diff --git a/src/cli/profile.ts b/src/cli/profile.ts index 8b831380fc2..d371efbba84 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -28,6 +28,18 @@ function takeValue( } const ARBITRARY_ARG_COMMAND_PATHS = [["nodes", "run"], ["docs"]] as const; +const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]); +const ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level"]); + +function isRootOptionValueToken(arg: string | undefined): boolean { + if (!arg || arg === "--") { + return false; + } + if (!arg.startsWith("-")) { + return true; + } + return /^-\d+(?:\.\d+)?$/.test(arg); +} function shouldGuardTrailingArgsFromProfileParsing(args: string[]): boolean { const commandTokens: string[] = []; @@ -38,12 +50,17 @@ function shouldGuardTrailingArgsFromProfileParsing(args: string[]): boolean { break; } - if (arg === "--dev") { + if (ROOT_BOOLEAN_FLAGS.has(arg)) { continue; } - if (arg === "--profile" || arg.startsWith("--profile=")) { - if (arg === "--profile") { + if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) { + continue; + } + + if (ROOT_VALUE_FLAGS.has(arg)) { + const next = args[i + 1]; + if (isRootOptionValueToken(next)) { i += 1; } continue; @@ -108,6 +125,18 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult { continue; } + if (arg === "--log-level" || arg.startsWith("--log-level=")) { + out.push(arg); + if (arg === "--log-level") { + const next = args[i + 1]; + if (isRootOptionValueToken(next)) { + out.push(next); + i += 1; + } + } + continue; + } + if ( sawCommand && (guardTrailingArgs || (arg !== "--profile" && !arg.startsWith("--profile=")))