Gateway: isolate dev reset from default profile state
This commit is contained in:
parent
97683071b5
commit
902396c8b6
@ -1,6 +1,6 @@
|
||||
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 +22,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 +33,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 +142,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 +169,10 @@ describe("gateway run option collisions", () => {
|
||||
);
|
||||
}
|
||||
|
||||
async function expectGatewayExit(argv: string[]) {
|
||||
await expect(runGatewayCli(argv)).rejects.toThrow("__exit__:1");
|
||||
}
|
||||
|
||||
it("forwards parent-captured options to `gateway run` subcommand", async () => {
|
||||
await runGatewayCli([
|
||||
"gateway",
|
||||
@ -303,4 +322,44 @@ describe("gateway run option collisions", () => {
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,40 @@ 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"),
|
||||
};
|
||||
}
|
||||
|
||||
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 +201,37 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.reset && devMode) {
|
||||
const paths = resolveDevResetPaths(process.env);
|
||||
const resolvedStateDir = path.resolve(paths.stateDir);
|
||||
const resolvedConfigPath = path.resolve(paths.configPath);
|
||||
const stateIsDefault = resolvedStateDir === path.resolve(paths.defaultStateDir);
|
||||
const configIsDefault = resolvedConfigPath === path.resolve(paths.defaultConfigPath);
|
||||
const stateMatchesDev = resolvedStateDir === path.resolve(paths.expectedDevStateDir);
|
||||
const configMatchesDev = resolvedConfigPath === path.resolve(paths.expectedDevConfigPath);
|
||||
const hasStateOverride = Boolean(process.env.OPENCLAW_STATE_DIR?.trim());
|
||||
const hasConfigOverride = Boolean(process.env.OPENCLAW_CONFIG_PATH?.trim());
|
||||
const hasExplicitCustomTarget =
|
||||
(hasStateOverride || hasConfigOverride) && !stateIsDefault && !configIsDefault;
|
||||
|
||||
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 +378,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,46 @@ 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("parses --profile value and strips it", () => {
|
||||
const res = parseCliProfileArgs(["node", "openclaw", "--profile", "work", "status"]);
|
||||
if (!res.ok) {
|
||||
@ -66,7 +106,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",
|
||||
@ -77,10 +117,42 @@ describe("applyCliProfileEnv", () => {
|
||||
homedir: () => "/home/peter",
|
||||
});
|
||||
expect(env.OPENCLAW_STATE_DIR).toBe("/custom");
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19099");
|
||||
// OPENCLAW_GATEWAY_PORT is intentionally reset for profile isolation.
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe("19001");
|
||||
expect(env.OPENCLAW_CONFIG_PATH).toBe(path.join("/custom", "openclaw.json"));
|
||||
});
|
||||
|
||||
it("clears inherited 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).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears inherited service env vars for profile isolation", () => {
|
||||
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).toBeUndefined();
|
||||
expect(env.OPENCLAW_LAUNCHD_LABEL).toBeUndefined();
|
||||
expect(env.OPENCLAW_SYSTEMD_UNIT).toBeUndefined();
|
||||
expect(env.OPENCLAW_SERVICE_VERSION).toBeUndefined();
|
||||
expect(env.OPENCLAW_PROFILE).toBe("work");
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_HOME when deriving profile state dir", () => {
|
||||
const env: Record<string, string | undefined> = {
|
||||
OPENCLAW_HOME: "/srv/openclaw-home",
|
||||
@ -100,6 +172,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,
|
||||
@ -40,7 +44,7 @@ export function parseCliProfileArgs(argv: string[]): CliProfileParseResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sawCommand) {
|
||||
if (sawCommand && arg !== "--profile" && !arg.startsWith("--profile=")) {
|
||||
out.push(arg);
|
||||
continue;
|
||||
}
|
||||
@ -88,6 +92,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>,
|
||||
@ -110,7 +135,12 @@ 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()) {
|
||||
@ -121,7 +151,7 @@ export function applyCliProfileEnv(params: {
|
||||
env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json");
|
||||
}
|
||||
|
||||
if (profile === "dev" && !env.OPENCLAW_GATEWAY_PORT?.trim()) {
|
||||
if (profile === "dev") {
|
||||
env.OPENCLAW_GATEWAY_PORT = "19001";
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
normalizeGatewayTokenInput,
|
||||
openUrl,
|
||||
resolveResetTargets,
|
||||
resolveBrowserOpenCommand,
|
||||
resolveControlUiLinks,
|
||||
validateGatewayPasswordInput,
|
||||
@ -153,3 +155,31 @@ 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"));
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 { resolveConfigPath, 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: resolveConfigPath(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 os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_GATEWAY_PORT,
|
||||
resolveDefaultConfigCandidates,
|
||||
resolveConfigPathCandidate,
|
||||
resolveConfigPath,
|
||||
resolveGatewayPort,
|
||||
resolveOAuthDir,
|
||||
resolveOAuthPath,
|
||||
resolveStateDir,
|
||||
@ -150,3 +152,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);
|
||||
});
|
||||
});
|
||||
|
||||
24
src/entry.ts
24
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";
|
||||
@ -112,7 +116,6 @@ if (
|
||||
}
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
child.once("error", (error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to respawn CLI:",
|
||||
@ -173,10 +176,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) && !tryHandleRootHelpFastPath(process.argv)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user