Merge 1fae5ec32f84dddae84f784b0c2da3550fd62540 into 598f1826d8b2bc969aace2c6459824737667218c
This commit is contained in:
commit
7c46e40af5
@ -1,6 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import fsPromises from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempSecretFiles } from "../../test-utils/secret-file-fixture.js";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
|
||||
|
||||
const startGatewayServer = vi.fn(async (_port: number, _opts?: unknown) => ({
|
||||
@ -22,6 +25,10 @@ const configState = vi.hoisted(() => ({
|
||||
cfg: {} as Record<string, unknown>,
|
||||
snapshot: { exists: false } as Record<string, unknown>,
|
||||
}));
|
||||
const resolveStateDir = vi.fn<(env?: NodeJS.ProcessEnv) => string>(() => "/tmp");
|
||||
const resolveConfigPath = vi.fn((_env: NodeJS.ProcessEnv, stateDir: string) => {
|
||||
return `${stateDir}/openclaw.json`;
|
||||
});
|
||||
|
||||
const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
|
||||
@ -29,7 +36,8 @@ vi.mock("../../config/config.js", () => ({
|
||||
getConfigPath: () => "/tmp/openclaw-test-missing-config.json",
|
||||
loadConfig: () => configState.cfg,
|
||||
readConfigFileSnapshot: async () => configState.snapshot,
|
||||
resolveStateDir: () => "/tmp",
|
||||
resolveConfigPath: (env: NodeJS.ProcessEnv, stateDir: string) => resolveConfigPath(env, stateDir),
|
||||
resolveStateDir: (env?: NodeJS.ProcessEnv) => resolveStateDir(env),
|
||||
resolveGatewayPort: () => 18789,
|
||||
}));
|
||||
|
||||
@ -137,6 +145,16 @@ describe("gateway run option collisions", () => {
|
||||
waitForPortBindable.mockClear();
|
||||
ensureDevGatewayConfig.mockClear();
|
||||
runGatewayLoop.mockClear();
|
||||
resolveStateDir.mockReset();
|
||||
resolveStateDir.mockReturnValue("/tmp");
|
||||
resolveConfigPath.mockReset();
|
||||
resolveConfigPath.mockImplementation((_env: NodeJS.ProcessEnv, stateDir: string) => {
|
||||
return `${stateDir}/openclaw.json`;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
async function runGatewayCli(argv: string[]) {
|
||||
@ -154,6 +172,24 @@ describe("gateway run option collisions", () => {
|
||||
);
|
||||
}
|
||||
|
||||
async function expectGatewayExit(argv: string[]) {
|
||||
await expect(runGatewayCli(argv)).rejects.toThrow("__exit__:1");
|
||||
}
|
||||
|
||||
async function withTempPasswordFile<T>(
|
||||
password: string,
|
||||
run: (params: { passwordFile: string }) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const dir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-run-"));
|
||||
const passwordFile = path.join(dir, "password.txt");
|
||||
try {
|
||||
await fsPromises.writeFile(passwordFile, password, "utf8");
|
||||
return await run({ passwordFile });
|
||||
} finally {
|
||||
await fsPromises.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
it("forwards parent-captured options to `gateway run` subcommand", async () => {
|
||||
await runGatewayCli([
|
||||
"gateway",
|
||||
@ -236,21 +272,17 @@ describe("gateway run option collisions", () => {
|
||||
});
|
||||
|
||||
it("reads gateway password from --password-file", async () => {
|
||||
await withTempSecretFiles(
|
||||
"openclaw-gateway-run-",
|
||||
{ password: "pw_from_file\n" },
|
||||
async ({ passwordFile }) => {
|
||||
await runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--auth",
|
||||
"password",
|
||||
"--password-file",
|
||||
passwordFile ?? "",
|
||||
"--allow-unconfigured",
|
||||
]);
|
||||
},
|
||||
);
|
||||
await withTempPasswordFile("pw_from_file\n", async ({ passwordFile }) => {
|
||||
await runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--auth",
|
||||
"password",
|
||||
"--password-file",
|
||||
passwordFile ?? "",
|
||||
"--allow-unconfigured",
|
||||
]);
|
||||
});
|
||||
|
||||
expect(startGatewayServer).toHaveBeenCalledWith(
|
||||
18789,
|
||||
@ -283,24 +315,155 @@ describe("gateway run option collisions", () => {
|
||||
});
|
||||
|
||||
it("rejects using both --password and --password-file", async () => {
|
||||
await withTempSecretFiles(
|
||||
"openclaw-gateway-run-",
|
||||
{ password: "pw_from_file\n" },
|
||||
async ({ passwordFile }) => {
|
||||
await expect(
|
||||
runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--password",
|
||||
"pw_inline",
|
||||
"--password-file",
|
||||
passwordFile ?? "",
|
||||
"--allow-unconfigured",
|
||||
]),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
},
|
||||
);
|
||||
await withTempPasswordFile("pw_from_file\n", async ({ passwordFile }) => {
|
||||
await expect(
|
||||
runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--password",
|
||||
"pw_inline",
|
||||
"--password-file",
|
||||
passwordFile ?? "",
|
||||
"--allow-unconfigured",
|
||||
]),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
});
|
||||
|
||||
expect(runtimeErrors).toContain("Use either --password or --password-file.");
|
||||
});
|
||||
|
||||
it("hard-stops --dev --reset when target resolves to default profile paths", async () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
resolveStateDir.mockReturnValue("/Users/test/.openclaw");
|
||||
resolveConfigPath.mockImplementation((_env: NodeJS.ProcessEnv, stateDir: string) => {
|
||||
return `${stateDir}/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.",
|
||||
);
|
||||
expect(runtimeErrors.join("\n")).toContain("/Users/test/.openclaw");
|
||||
});
|
||||
|
||||
it("allows --dev --reset when target resolves to dev profile paths", async () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
resolveStateDir.mockReturnValue("/Users/test/.openclaw-dev");
|
||||
resolveConfigPath.mockImplementation((_env: NodeJS.ProcessEnv, stateDir: string) => {
|
||||
return `${stateDir}/openclaw.json`;
|
||||
});
|
||||
|
||||
await runGatewayCli(["gateway", "run", "--dev", "--reset", "--allow-unconfigured"]);
|
||||
|
||||
expect(ensureDevGatewayConfig).toHaveBeenCalledWith({ reset: true });
|
||||
});
|
||||
|
||||
it("allows --dev --reset for explicit non-default custom state/config paths", async () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", "/tmp/custom-dev");
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", "/tmp/custom-dev/openclaw.json");
|
||||
resolveStateDir.mockReturnValue("/tmp/custom-dev");
|
||||
resolveConfigPath.mockReturnValue("/tmp/custom-dev/openclaw.json");
|
||||
|
||||
await runGatewayCli(["gateway", "run", "--dev", "--reset", "--allow-unconfigured"]);
|
||||
|
||||
expect(ensureDevGatewayConfig).toHaveBeenCalledWith({ reset: true });
|
||||
});
|
||||
|
||||
it("allows --dev --reset for explicit non-default legacy state/config paths", async () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
vi.stubEnv("CLAWDBOT_STATE_DIR", "/tmp/custom-dev");
|
||||
vi.stubEnv("CLAWDBOT_CONFIG_PATH", "/tmp/custom-dev/openclaw.json");
|
||||
resolveStateDir.mockReturnValue("/tmp/custom-dev");
|
||||
resolveConfigPath.mockReturnValue("/tmp/custom-dev/openclaw.json");
|
||||
|
||||
await runGatewayCli(["gateway", "run", "--dev", "--reset", "--allow-unconfigured"]);
|
||||
|
||||
expect(ensureDevGatewayConfig).toHaveBeenCalledWith({ reset: true });
|
||||
});
|
||||
|
||||
it("hard-stops --dev --reset when state/config match non-dev profile defaults", 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", "/Users/test/.openclaw-work/openclaw.json");
|
||||
resolveStateDir.mockReturnValue("/Users/test/.openclaw-work");
|
||||
resolveConfigPath.mockReturnValue("/Users/test/.openclaw-work/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("hard-stops --dev --reset when legacy env resolves to non-dev profile defaults", async () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
vi.stubEnv("OPENCLAW_PROFILE", "work");
|
||||
vi.stubEnv("CLAWDBOT_STATE_DIR", "/Users/test/.openclaw-work");
|
||||
vi.stubEnv("CLAWDBOT_CONFIG_PATH", "/Users/test/.openclaw-work/openclaw.json");
|
||||
resolveStateDir.mockReturnValue("/Users/test/.openclaw-work");
|
||||
resolveConfigPath.mockReturnValue("/Users/test/.openclaw-work/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("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");
|
||||
const defaultConfigPath = path.join(defaultStateDir, "openclaw.json");
|
||||
const aliasStateDir = path.join(home, ".openclaw-alias");
|
||||
const aliasConfigPath = path.join(aliasStateDir, "openclaw.json");
|
||||
const realpathSpy = vi.spyOn(fs, "realpathSync").mockImplementation((candidate) => {
|
||||
const resolved = path.resolve(String(candidate));
|
||||
if (resolved === path.resolve(aliasStateDir)) {
|
||||
return path.resolve(defaultStateDir);
|
||||
}
|
||||
if (resolved === path.resolve(aliasConfigPath)) {
|
||||
return path.resolve(defaultConfigPath);
|
||||
}
|
||||
return resolved;
|
||||
});
|
||||
|
||||
vi.stubEnv("HOME", home);
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", aliasStateDir);
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", aliasConfigPath);
|
||||
resolveStateDir.mockReturnValue(aliasStateDir);
|
||||
resolveConfigPath.mockReturnValue(aliasConfigPath);
|
||||
|
||||
try {
|
||||
await expectGatewayExit(["gateway", "run", "--dev", "--reset"]);
|
||||
} finally {
|
||||
realpathSpy.mockRestore();
|
||||
}
|
||||
|
||||
expect(ensureDevGatewayConfig).not.toHaveBeenCalled();
|
||||
expect(runtimeErrors.join("\n")).toContain(
|
||||
"Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Command } from "commander";
|
||||
import { readSecretFromFile } from "../../acp/secret-file.js";
|
||||
import type { GatewayAuthMode, GatewayTailscaleMode } from "../../config/config.js";
|
||||
import {
|
||||
CONFIG_PATH,
|
||||
loadConfig,
|
||||
readConfigFileSnapshot,
|
||||
resolveConfigPath,
|
||||
resolveStateDir,
|
||||
resolveGatewayPort,
|
||||
} from "../../config/config.js";
|
||||
@ -17,6 +18,7 @@ import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||
import { setVerbose } from "../../globals.js";
|
||||
import { GatewayLockError } from "../../infra/gateway-lock.js";
|
||||
import { resolveRequiredHomeDir } from "../../infra/home-dir.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
|
||||
import { cleanStaleGatewayProcessesSync } from "../../infra/restart-stale-pids.js";
|
||||
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
|
||||
@ -25,6 +27,7 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { inheritOptionFromParent } from "../command-options.js";
|
||||
import { forceFreePortAndWait, waitForPortBindable } from "../ports.js";
|
||||
import { isValidProfileName } from "../profile-utils.js";
|
||||
import { ensureDevGatewayConfig } from "./dev.js";
|
||||
import { runGatewayLoop } from "./run-loop.js";
|
||||
import {
|
||||
@ -157,8 +160,65 @@ function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): Gate
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveDevResetPaths(env: NodeJS.ProcessEnv = process.env): {
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
defaultStateDir: string;
|
||||
defaultConfigPath: string;
|
||||
expectedDevStateDir: string;
|
||||
expectedDevConfigPath: string;
|
||||
} {
|
||||
const home = resolveRequiredHomeDir(env, os.homedir);
|
||||
const stateDir = resolveStateDir(env);
|
||||
const configPath = resolveConfigPath(env, stateDir);
|
||||
const defaultStateDir = path.join(home, ".openclaw");
|
||||
const expectedDevStateDir = path.join(home, ".openclaw-dev");
|
||||
return {
|
||||
stateDir,
|
||||
configPath,
|
||||
defaultStateDir,
|
||||
defaultConfigPath: path.join(defaultStateDir, "openclaw.json"),
|
||||
expectedDevStateDir,
|
||||
expectedDevConfigPath: path.join(expectedDevStateDir, "openclaw.json"),
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalizePathForCompare(rawPath: string): string {
|
||||
const resolvedPath = path.resolve(rawPath);
|
||||
try {
|
||||
return fs.realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProfileDefaultPaths(
|
||||
profile: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): {
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
} {
|
||||
const home = resolveRequiredHomeDir(env, os.homedir);
|
||||
const suffix = profile.toLowerCase() === "default" ? "" : `-${profile}`;
|
||||
const stateDir = path.join(home, `.openclaw${suffix}`);
|
||||
return {
|
||||
stateDir,
|
||||
configPath: path.join(stateDir, "openclaw.json"),
|
||||
};
|
||||
}
|
||||
|
||||
async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
const isDevProfile = process.env.OPENCLAW_PROFILE?.trim().toLowerCase() === "dev";
|
||||
const envProfile = process.env.OPENCLAW_PROFILE?.trim();
|
||||
if (envProfile && !isValidProfileName(envProfile)) {
|
||||
defaultRuntime.error(
|
||||
'Invalid OPENCLAW_PROFILE (use letters, numbers, "_", "-" only, or unset the variable).',
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDevProfile = envProfile?.toLowerCase() === "dev";
|
||||
const devMode = Boolean(opts.dev) || isDevProfile;
|
||||
if (opts.reset && !devMode) {
|
||||
defaultRuntime.error("Use --reset with --dev.");
|
||||
@ -166,6 +226,57 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.reset && devMode) {
|
||||
const paths = resolveDevResetPaths(process.env);
|
||||
const resolvedStateDir = canonicalizePathForCompare(paths.stateDir);
|
||||
const resolvedConfigPath = canonicalizePathForCompare(paths.configPath);
|
||||
const stateIsDefault = resolvedStateDir === canonicalizePathForCompare(paths.defaultStateDir);
|
||||
const configIsDefault =
|
||||
resolvedConfigPath === canonicalizePathForCompare(paths.defaultConfigPath);
|
||||
const stateMatchesDev =
|
||||
resolvedStateDir === canonicalizePathForCompare(paths.expectedDevStateDir);
|
||||
const configMatchesDev =
|
||||
resolvedConfigPath === canonicalizePathForCompare(paths.expectedDevConfigPath);
|
||||
const profileDefaultPaths = envProfile
|
||||
? resolveProfileDefaultPaths(envProfile, process.env)
|
||||
: null;
|
||||
const stateMatchesProfileDefault = profileDefaultPaths
|
||||
? resolvedStateDir === canonicalizePathForCompare(profileDefaultPaths.stateDir)
|
||||
: false;
|
||||
const configMatchesProfileDefault = profileDefaultPaths
|
||||
? resolvedConfigPath === canonicalizePathForCompare(profileDefaultPaths.configPath)
|
||||
: false;
|
||||
const stateTargetsProfileDefault = stateIsDefault || stateMatchesProfileDefault;
|
||||
const configTargetsProfileDefault = configIsDefault || configMatchesProfileDefault;
|
||||
const hasStateOverride = Boolean(
|
||||
process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim(),
|
||||
);
|
||||
const hasConfigOverride = Boolean(
|
||||
process.env.OPENCLAW_CONFIG_PATH?.trim() || process.env.CLAWDBOT_CONFIG_PATH?.trim(),
|
||||
);
|
||||
const hasExplicitCustomTarget =
|
||||
hasStateOverride &&
|
||||
!stateTargetsProfileDefault &&
|
||||
(!hasConfigOverride || !configTargetsProfileDefault);
|
||||
|
||||
if (!hasExplicitCustomTarget && (!stateMatchesDev || !configMatchesDev)) {
|
||||
defaultRuntime.error(
|
||||
[
|
||||
"Refusing to run `gateway --dev --reset` because the reset target is not dev-isolated.",
|
||||
`Resolved state dir: ${paths.stateDir}`,
|
||||
`Resolved config path: ${paths.configPath}`,
|
||||
`Expected dev state dir: ${paths.expectedDevStateDir}`,
|
||||
`Expected dev config path: ${paths.expectedDevConfigPath}`,
|
||||
"Retry with:",
|
||||
" openclaw --dev gateway --dev --reset",
|
||||
` OPENCLAW_STATE_DIR="${paths.expectedDevStateDir}" OPENCLAW_CONFIG_PATH="${paths.expectedDevConfigPath}" openclaw gateway --dev --reset`,
|
||||
].join("\n"),
|
||||
);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setConsoleTimestampPrefix(true);
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
if (opts.claudeCliLogs) {
|
||||
@ -312,8 +423,10 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
const tokenRaw = toOptionString(opts.token);
|
||||
|
||||
const snapshot = await readConfigFileSnapshot().catch(() => null);
|
||||
const configExists = snapshot?.exists ?? fs.existsSync(CONFIG_PATH);
|
||||
const configAuditPath = path.join(resolveStateDir(process.env), "logs", "config-audit.jsonl");
|
||||
const stateDir = resolveStateDir(process.env);
|
||||
const configPath = resolveConfigPath(process.env, stateDir);
|
||||
const configExists = snapshot?.exists ?? fs.existsSync(configPath);
|
||||
const configAuditPath = path.join(stateDir, "logs", "config-audit.jsonl");
|
||||
const mode = cfg.gateway?.mode;
|
||||
if (!opts.allowUnconfigured && mode !== "local") {
|
||||
if (!configExists) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs, resolveEffectiveCliProfile } from "./profile.js";
|
||||
|
||||
describe("parseCliProfileArgs", () => {
|
||||
it("leaves gateway --dev for subcommands", () => {
|
||||
@ -28,6 +28,243 @@ describe("parseCliProfileArgs", () => {
|
||||
expect(res.argv).toEqual(["node", "openclaw", "gateway"]);
|
||||
});
|
||||
|
||||
it("parses --profile after subcommand", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"openclaw",
|
||||
"gateway",
|
||||
"--profile",
|
||||
"invest",
|
||||
"--port",
|
||||
"18795",
|
||||
]);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBe("invest");
|
||||
expect(res.argv).toEqual(["node", "openclaw", "gateway", "--port", "18795"]);
|
||||
});
|
||||
|
||||
it("parses --profile=NAME after subcommand", () => {
|
||||
const res = parseCliProfileArgs(["node", "openclaw", "gateway", "--profile=work"]);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBe("work");
|
||||
expect(res.argv).toEqual(["node", "openclaw", "gateway"]);
|
||||
});
|
||||
|
||||
it("keeps --dev and strips --profile when both appear after subcommand", () => {
|
||||
const res = parseCliProfileArgs(["node", "openclaw", "gateway", "--dev", "--profile", "work"]);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBe("work");
|
||||
expect(res.argv).toEqual(["node", "openclaw", "gateway", "--dev"]);
|
||||
});
|
||||
|
||||
it("rejects --dev before subcommand combined with --profile after subcommand", () => {
|
||||
const res = parseCliProfileArgs(["node", "openclaw", "--dev", "gateway", "--profile", "work"]);
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("does not intercept --profile after 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 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("does not intercept --profile in acp client server args", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"openclaw",
|
||||
"acp",
|
||||
"client",
|
||||
"--server-args",
|
||||
"--profile",
|
||||
"work",
|
||||
]);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBeNull();
|
||||
expect(res.argv).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"acp",
|
||||
"client",
|
||||
"--server-args",
|
||||
"--profile",
|
||||
"work",
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves acp client server args after root option values", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"openclaw",
|
||||
"--log-level",
|
||||
"debug",
|
||||
"acp",
|
||||
"client",
|
||||
"--server-args",
|
||||
"--profile",
|
||||
"work",
|
||||
]);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBeNull();
|
||||
expect(res.argv).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"--log-level",
|
||||
"debug",
|
||||
"acp",
|
||||
"client",
|
||||
"--server-args",
|
||||
"--profile",
|
||||
"work",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps passthrough --profile when global --profile is set before terminator", () => {
|
||||
const res = parseCliProfileArgs([
|
||||
"node",
|
||||
"openclaw",
|
||||
"--profile",
|
||||
"work",
|
||||
"nodes",
|
||||
"run",
|
||||
"--",
|
||||
"aws",
|
||||
"--profile=prod",
|
||||
"sts",
|
||||
"get-caller-identity",
|
||||
]);
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
expect(res.profile).toBe("work");
|
||||
expect(res.argv).toEqual([
|
||||
"node",
|
||||
"openclaw",
|
||||
"nodes",
|
||||
"run",
|
||||
"--",
|
||||
"aws",
|
||||
"--profile=prod",
|
||||
"sts",
|
||||
"get-caller-identity",
|
||||
]);
|
||||
});
|
||||
|
||||
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) {
|
||||
@ -66,7 +303,7 @@ describe("applyCliProfileEnv", () => {
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19001");
|
||||
});
|
||||
|
||||
it("does not override explicit env values", () => {
|
||||
it("does not override explicit OPENCLAW_STATE_DIR env value", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_STATE_DIR: "/custom",
|
||||
OPENCLAW_GATEWAY_PORT: "19099",
|
||||
@ -81,6 +318,47 @@ describe("applyCliProfileEnv", () => {
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBe(path.join("/custom", "openclaw.json"));
|
||||
});
|
||||
|
||||
it("preserves explicit OPENCLAW_GATEWAY_PORT for non-dev profiles", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_GATEWAY_PORT: "18789",
|
||||
};
|
||||
applyCliProfileEnv({
|
||||
profile: "work",
|
||||
env,
|
||||
homedir: () => "/home/peter",
|
||||
});
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe("18789");
|
||||
});
|
||||
|
||||
it("preserves service override env vars", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_GATEWAY_PORT: "18789",
|
||||
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway",
|
||||
OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway.service",
|
||||
OPENCLAW_SERVICE_VERSION: "2026.1.0",
|
||||
};
|
||||
applyCliProfileEnv({
|
||||
profile: "work",
|
||||
env,
|
||||
homedir: () => "/home/peter",
|
||||
});
|
||||
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",
|
||||
@ -100,6 +378,40 @@ describe("applyCliProfileEnv", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEffectiveCliProfile", () => {
|
||||
it("prefers parsed profile over env profile", () => {
|
||||
const res = resolveEffectiveCliProfile({
|
||||
parsedProfile: "work",
|
||||
envProfile: "dev",
|
||||
});
|
||||
expect(res).toEqual({ ok: true, profile: "work" });
|
||||
});
|
||||
|
||||
it("falls back to env profile when parsed profile is absent", () => {
|
||||
const res = resolveEffectiveCliProfile({
|
||||
parsedProfile: null,
|
||||
envProfile: "dev",
|
||||
});
|
||||
expect(res).toEqual({ ok: true, profile: "dev" });
|
||||
});
|
||||
|
||||
it("treats blank env profile as unset", () => {
|
||||
const res = resolveEffectiveCliProfile({
|
||||
parsedProfile: null,
|
||||
envProfile: " ",
|
||||
});
|
||||
expect(res).toEqual({ ok: true, profile: null });
|
||||
});
|
||||
|
||||
it("rejects invalid env profile values", () => {
|
||||
const res = resolveEffectiveCliProfile({
|
||||
parsedProfile: null,
|
||||
envProfile: "bad profile",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCliCommand", () => {
|
||||
it.each([
|
||||
{
|
||||
|
||||
@ -7,6 +7,10 @@ export type CliProfileParseResult =
|
||||
| { ok: true; profile: string | null; argv: string[] }
|
||||
| { ok: false; error: string };
|
||||
|
||||
export type EffectiveCliProfileResult =
|
||||
| { ok: true; profile: string | null }
|
||||
| { ok: false; error: string };
|
||||
|
||||
function takeValue(
|
||||
raw: string,
|
||||
next: string | undefined,
|
||||
@ -23,6 +27,74 @@ function takeValue(
|
||||
return { value: trimmed || null, consumedNext: Boolean(next) };
|
||||
}
|
||||
|
||||
const ARBITRARY_ARG_COMMAND_PATHS = [["nodes", "run"], ["docs"], ["acp", "client"]] 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[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg || arg === "--") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (arg.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
commandTokens.push(arg);
|
||||
|
||||
if (
|
||||
ARBITRARY_ARG_COMMAND_PATHS.some(
|
||||
(commandPath) =>
|
||||
commandPath.length === commandTokens.length &&
|
||||
commandPath.every((part, idx) => part === commandTokens[idx]),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const couldStillMatch = ARBITRARY_ARG_COMMAND_PATHS.some(
|
||||
(commandPath) =>
|
||||
commandTokens.length <= commandPath.length &&
|
||||
commandTokens.every((part, idx) => commandPath[idx] === part),
|
||||
);
|
||||
if (!couldStillMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
if (argv.length < 2) {
|
||||
return { ok: true, profile: null, argv };
|
||||
@ -32,15 +104,43 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
let profile: string | null = null;
|
||||
let sawDev = false;
|
||||
let sawCommand = false;
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sawCommand) {
|
||||
if (sawTerminator) {
|
||||
out.push(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--") {
|
||||
sawTerminator = true;
|
||||
out.push(arg);
|
||||
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=")))
|
||||
) {
|
||||
out.push(arg);
|
||||
continue;
|
||||
}
|
||||
@ -88,6 +188,27 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
return { ok: true, profile, argv: out };
|
||||
}
|
||||
|
||||
export function resolveEffectiveCliProfile(params: {
|
||||
parsedProfile: string | null;
|
||||
envProfile?: string;
|
||||
}): EffectiveCliProfileResult {
|
||||
if (params.parsedProfile) {
|
||||
return { ok: true, profile: params.parsedProfile };
|
||||
}
|
||||
const envProfile = params.envProfile?.trim() || "";
|
||||
if (!envProfile) {
|
||||
return { ok: true, profile: null };
|
||||
}
|
||||
if (!isValidProfileName(envProfile)) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
'Invalid OPENCLAW_PROFILE (use letters, numbers, "_", "-" only, or unset the variable)',
|
||||
};
|
||||
}
|
||||
return { ok: true, profile: envProfile };
|
||||
}
|
||||
|
||||
function resolveProfileStateDir(
|
||||
profile: string,
|
||||
env: Record<string, string | undefined>,
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
normalizeGatewayTokenInput,
|
||||
openUrl,
|
||||
resolveBrowserOpenCommand,
|
||||
resolveControlUiLinks,
|
||||
resolveResetTargets,
|
||||
validateGatewayPasswordInput,
|
||||
} from "./onboard-helpers.js";
|
||||
|
||||
@ -23,6 +25,11 @@ const mocks = vi.hoisted(() => ({
|
||||
pickPrimaryTailnetIPv4: vi.fn<() => string | undefined>(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@mariozechner/pi-ai", () => ({
|
||||
getOAuthProviders: () => [],
|
||||
getOAuthApiKey: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: mocks.runCommandWithTimeout,
|
||||
}));
|
||||
@ -153,3 +160,44 @@ describe("validateGatewayPasswordInput", () => {
|
||||
expect(validateGatewayPasswordInput(" secret ")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveResetTargets", () => {
|
||||
it("derives reset targets from the active OPENCLAW_STATE_DIR", () => {
|
||||
const env = {
|
||||
OPENCLAW_STATE_DIR: "/tmp/custom-openclaw",
|
||||
} as NodeJS.ProcessEnv;
|
||||
const targets = resolveResetTargets(env);
|
||||
|
||||
const expectedStateDir = path.resolve("/tmp/custom-openclaw");
|
||||
expect(targets.stateDir).toBe(expectedStateDir);
|
||||
expect(targets.configPath).toBe(path.join(expectedStateDir, "openclaw.json"));
|
||||
expect(targets.credentialsPath).toBe(path.join(expectedStateDir, "credentials"));
|
||||
expect(targets.sessionsDir).toBe(path.join(expectedStateDir, "agents", "main", "sessions"));
|
||||
});
|
||||
|
||||
it("respects OPENCLAW_CONFIG_PATH override at runtime", () => {
|
||||
const env = {
|
||||
OPENCLAW_STATE_DIR: "/tmp/custom-openclaw",
|
||||
OPENCLAW_CONFIG_PATH: "/tmp/alternate/openclaw.json",
|
||||
} as NodeJS.ProcessEnv;
|
||||
const targets = resolveResetTargets(env);
|
||||
|
||||
expect(targets.stateDir).toBe(path.resolve("/tmp/custom-openclaw"));
|
||||
expect(targets.configPath).toBe(path.resolve("/tmp/alternate/openclaw.json"));
|
||||
expect(targets.credentialsPath).toBe(path.resolve("/tmp/custom-openclaw/credentials"));
|
||||
expect(targets.sessionsDir).toBe(path.resolve("/tmp/custom-openclaw/agents/main/sessions"));
|
||||
});
|
||||
|
||||
it("respects legacy CLAWDBOT_CONFIG_PATH override at runtime", () => {
|
||||
const env = {
|
||||
OPENCLAW_STATE_DIR: "/tmp/custom-openclaw",
|
||||
CLAWDBOT_CONFIG_PATH: "/tmp/legacy/openclaw.json",
|
||||
} as NodeJS.ProcessEnv;
|
||||
const targets = resolveResetTargets(env);
|
||||
|
||||
expect(targets.stateDir).toBe(path.resolve("/tmp/custom-openclaw"));
|
||||
expect(targets.configPath).toBe(path.resolve("/tmp/legacy/openclaw.json"));
|
||||
expect(targets.credentialsPath).toBe(path.resolve("/tmp/custom-openclaw/credentials"));
|
||||
expect(targets.sessionsDir).toBe(path.resolve("/tmp/custom-openclaw/agents/main/sessions"));
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@ import { inspect } from "node:util";
|
||||
import { cancel, isCancel } from "@clack/prompts";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/workspace.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { CONFIG_PATH } from "../config/config.js";
|
||||
import { resolveCanonicalConfigPath, resolveStateDir } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
@ -17,13 +17,7 @@ import { isWSL } from "../infra/wsl.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import {
|
||||
CONFIG_DIR,
|
||||
resolveUserPath,
|
||||
shortenHomeInString,
|
||||
shortenHomePath,
|
||||
sleep,
|
||||
} from "../utils.js";
|
||||
import { resolveUserPath, shortenHomeInString, shortenHomePath, sleep } from "../utils.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { NodeManagerChoice, OnboardMode, ResetScope } from "./onboard-types.js";
|
||||
@ -329,13 +323,32 @@ export async function moveToTrash(pathname: string, runtime: RuntimeEnv): Promis
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveResetTargets(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
agentId?: string,
|
||||
): {
|
||||
stateDir: string;
|
||||
configPath: string;
|
||||
credentialsPath: string;
|
||||
sessionsDir: string;
|
||||
} {
|
||||
const stateDir = resolveStateDir(env);
|
||||
return {
|
||||
stateDir,
|
||||
configPath: resolveCanonicalConfigPath(env, stateDir),
|
||||
credentialsPath: path.join(stateDir, "credentials"),
|
||||
sessionsDir: resolveSessionTranscriptsDirForAgent(agentId, env),
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleReset(scope: ResetScope, workspaceDir: string, runtime: RuntimeEnv) {
|
||||
await moveToTrash(CONFIG_PATH, runtime);
|
||||
const targets = resolveResetTargets(process.env);
|
||||
await moveToTrash(targets.configPath, runtime);
|
||||
if (scope === "config") {
|
||||
return;
|
||||
}
|
||||
await moveToTrash(path.join(CONFIG_DIR, "credentials"), runtime);
|
||||
await moveToTrash(resolveSessionTranscriptsDirForAgent(), runtime);
|
||||
await moveToTrash(targets.credentialsPath, runtime);
|
||||
await moveToTrash(targets.sessionsDir, runtime);
|
||||
if (scope === "full") {
|
||||
await moveToTrash(workspaceDir, runtime);
|
||||
}
|
||||
|
||||
@ -3,9 +3,11 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_PORT,
|
||||
resolveDefaultConfigCandidates,
|
||||
resolveConfigPathCandidate,
|
||||
resolveConfigPath,
|
||||
resolveGatewayPort,
|
||||
resolveOAuthDir,
|
||||
resolveOAuthPath,
|
||||
resolveStateDir,
|
||||
@ -141,3 +143,44 @@ describe("state + config path candidates", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewayPort", () => {
|
||||
it("returns DEFAULT_GATEWAY_PORT when no env or config port is set", () => {
|
||||
expect(resolveGatewayPort(undefined, {} as NodeJS.ProcessEnv)).toBe(DEFAULT_GATEWAY_PORT);
|
||||
});
|
||||
|
||||
it("prefers OPENCLAW_GATEWAY_PORT env var over config", () => {
|
||||
const env = { OPENCLAW_GATEWAY_PORT: "18790" } as NodeJS.ProcessEnv;
|
||||
const cfg = { gateway: { port: 19001 } } as Parameters<typeof resolveGatewayPort>[0];
|
||||
expect(resolveGatewayPort(cfg, env)).toBe(18790);
|
||||
});
|
||||
|
||||
it("prefers CLAWDBOT_GATEWAY_PORT env var over config", () => {
|
||||
const env = { CLAWDBOT_GATEWAY_PORT: "18791" } as NodeJS.ProcessEnv;
|
||||
const cfg = { gateway: { port: 19001 } } as Parameters<typeof resolveGatewayPort>[0];
|
||||
expect(resolveGatewayPort(cfg, env)).toBe(18791);
|
||||
});
|
||||
|
||||
it("falls back to config port when env var is absent", () => {
|
||||
const cfg = { gateway: { port: 18790 } } as Parameters<typeof resolveGatewayPort>[0];
|
||||
expect(resolveGatewayPort(cfg, {} as NodeJS.ProcessEnv)).toBe(18790);
|
||||
});
|
||||
|
||||
it("falls back to DEFAULT_GATEWAY_PORT when env and config are both missing", () => {
|
||||
expect(
|
||||
resolveGatewayPort({} as Parameters<typeof resolveGatewayPort>[0], {} as NodeJS.ProcessEnv),
|
||||
).toBe(DEFAULT_GATEWAY_PORT);
|
||||
});
|
||||
|
||||
it("ignores invalid env ports and falls back to config", () => {
|
||||
const env = { OPENCLAW_GATEWAY_PORT: "abc" } as NodeJS.ProcessEnv;
|
||||
const cfg = { gateway: { port: 18790 } } as Parameters<typeof resolveGatewayPort>[0];
|
||||
expect(resolveGatewayPort(cfg, env)).toBe(18790);
|
||||
});
|
||||
|
||||
it("ignores zero env ports and falls back to config", () => {
|
||||
const env = { OPENCLAW_GATEWAY_PORT: "0" } as NodeJS.ProcessEnv;
|
||||
const cfg = { gateway: { port: 18790 } } as Parameters<typeof resolveGatewayPort>[0];
|
||||
expect(resolveGatewayPort(cfg, env)).toBe(18790);
|
||||
});
|
||||
});
|
||||
|
||||
23
src/entry.ts
23
src/entry.ts
@ -4,7 +4,11 @@ import { enableCompileCache } from "node:module";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isRootHelpInvocation, isRootVersionInvocation } from "./cli/argv.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
|
||||
import {
|
||||
applyCliProfileEnv,
|
||||
parseCliProfileArgs,
|
||||
resolveEffectiveCliProfile,
|
||||
} from "./cli/profile.js";
|
||||
import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js";
|
||||
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
|
||||
import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
|
||||
@ -158,10 +162,21 @@ if (
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (parsed.profile) {
|
||||
applyCliProfileEnv({ profile: parsed.profile });
|
||||
const effectiveProfile = resolveEffectiveCliProfile({
|
||||
parsedProfile: parsed.profile,
|
||||
envProfile: process.env.OPENCLAW_PROFILE,
|
||||
});
|
||||
if (!effectiveProfile.ok) {
|
||||
console.error(`[openclaw] ${effectiveProfile.error}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (effectiveProfile.profile) {
|
||||
applyCliProfileEnv({ profile: effectiveProfile.profile });
|
||||
// Keep Commander and ad-hoc argv checks consistent.
|
||||
process.argv = parsed.argv;
|
||||
if (parsed.profile) {
|
||||
process.argv = parsed.argv;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tryHandleRootVersionFastPath(process.argv)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user