diff --git a/src/cli/run-main.profile-env.test.ts b/src/cli/run-main.profile-env.test.ts new file mode 100644 index 00000000000..cd3dde3a93d --- /dev/null +++ b/src/cli/run-main.profile-env.test.ts @@ -0,0 +1,79 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const dotenvState = vi.hoisted(() => { + const state = { + profileAtDotenvLoad: undefined as string | undefined, + }; + return { + state, + loadDotEnv: vi.fn(() => { + state.profileAtDotenvLoad = process.env.OPENCLAW_PROFILE; + }), + }; +}); + +vi.mock("../infra/dotenv.js", () => ({ + loadDotEnv: dotenvState.loadDotEnv, +})); + +vi.mock("../infra/env.js", () => ({ + normalizeEnv: vi.fn(), +})); + +vi.mock("../infra/runtime-guard.js", () => ({ + assertSupportedRuntime: vi.fn(), +})); + +vi.mock("../infra/path-env.js", () => ({ + ensureOpenClawCliOnPath: vi.fn(), +})); + +vi.mock("./route.js", () => ({ + tryRouteCli: vi.fn(async () => true), +})); + +vi.mock("./windows-argv.js", () => ({ + normalizeWindowsArgv: (argv: string[]) => argv, +})); + +import { runCli } from "./run-main.js"; + +describe("runCli profile env bootstrap", () => { + const originalProfile = process.env.OPENCLAW_PROFILE; + const originalStateDir = process.env.OPENCLAW_STATE_DIR; + const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH; + + beforeEach(() => { + delete process.env.OPENCLAW_PROFILE; + delete process.env.OPENCLAW_STATE_DIR; + delete process.env.OPENCLAW_CONFIG_PATH; + dotenvState.state.profileAtDotenvLoad = undefined; + dotenvState.loadDotEnv.mockClear(); + }); + + afterEach(() => { + if (originalProfile === undefined) { + delete process.env.OPENCLAW_PROFILE; + } else { + process.env.OPENCLAW_PROFILE = originalProfile; + } + if (originalStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalStateDir; + } + if (originalConfigPath === undefined) { + delete process.env.OPENCLAW_CONFIG_PATH; + } else { + process.env.OPENCLAW_CONFIG_PATH = originalConfigPath; + } + }); + + it("applies --profile before dotenv loading", async () => { + await runCli(["node", "openclaw", "--profile", "rawdog", "status"]); + + expect(dotenvState.loadDotEnv).toHaveBeenCalledOnce(); + expect(dotenvState.state.profileAtDotenvLoad).toBe("rawdog"); + expect(process.env.OPENCLAW_PROFILE).toBe("rawdog"); + }); +}); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 0d0eee78250..4f78c82bd4d 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -9,6 +9,7 @@ import { assertSupportedRuntime } from "../infra/runtime-guard.js"; import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js"; import { enableConsoleCapture } from "../logging.js"; import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js"; +import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js"; import { tryRouteCli } from "./route.js"; import { normalizeWindowsArgv } from "./windows-argv.js"; @@ -62,7 +63,16 @@ export function shouldEnsureCliPath(argv: string[]): boolean { } export async function runCli(argv: string[] = process.argv) { - const normalizedArgv = normalizeWindowsArgv(argv); + let normalizedArgv = normalizeWindowsArgv(argv); + const parsedProfile = parseCliProfileArgs(normalizedArgv); + if (!parsedProfile.ok) { + throw new Error(parsedProfile.error); + } + if (parsedProfile.profile) { + applyCliProfileEnv({ profile: parsedProfile.profile }); + } + normalizedArgv = parsedProfile.argv; + loadDotEnv({ quiet: true }); normalizeEnv(); if (shouldEnsureCliPath(normalizedArgv)) {