mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
refactor: rename to openclaw
This commit is contained in:
@@ -14,7 +14,7 @@ describe("resolveGatewayLaunchAgentLabel", () => {
|
||||
it("returns default label when no profile is set", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel();
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
expect(result).toBe("bot.molt.gateway");
|
||||
expect(result).toBe("ai.openclaw.gateway");
|
||||
});
|
||||
|
||||
it("returns default label when profile is undefined", () => {
|
||||
@@ -34,17 +34,17 @@ describe("resolveGatewayLaunchAgentLabel", () => {
|
||||
|
||||
it("returns profile-specific label when profile is set", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("dev");
|
||||
expect(result).toBe("bot.molt.dev");
|
||||
expect(result).toBe("ai.openclaw.dev");
|
||||
});
|
||||
|
||||
it("returns profile-specific label for custom profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("work");
|
||||
expect(result).toBe("bot.molt.work");
|
||||
expect(result).toBe("ai.openclaw.work");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(" staging ");
|
||||
expect(result).toBe("bot.molt.staging");
|
||||
expect(result).toBe("ai.openclaw.staging");
|
||||
});
|
||||
|
||||
it("returns default label for empty string profile", () => {
|
||||
@@ -62,7 +62,7 @@ describe("resolveGatewaySystemdServiceName", () => {
|
||||
it("returns default service name when no profile is set", () => {
|
||||
const result = resolveGatewaySystemdServiceName();
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
expect(result).toBe("moltbot-gateway");
|
||||
expect(result).toBe("openclaw-gateway");
|
||||
});
|
||||
|
||||
it("returns default service name when profile is undefined", () => {
|
||||
@@ -82,17 +82,17 @@ describe("resolveGatewaySystemdServiceName", () => {
|
||||
|
||||
it("returns profile-specific service name when profile is set", () => {
|
||||
const result = resolveGatewaySystemdServiceName("dev");
|
||||
expect(result).toBe("moltbot-gateway-dev");
|
||||
expect(result).toBe("openclaw-gateway-dev");
|
||||
});
|
||||
|
||||
it("returns profile-specific service name for custom profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName("production");
|
||||
expect(result).toBe("moltbot-gateway-production");
|
||||
expect(result).toBe("openclaw-gateway-production");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName(" test ");
|
||||
expect(result).toBe("moltbot-gateway-test");
|
||||
expect(result).toBe("openclaw-gateway-test");
|
||||
});
|
||||
|
||||
it("returns default service name for empty string profile", () => {
|
||||
@@ -110,7 +110,7 @@ describe("resolveGatewayWindowsTaskName", () => {
|
||||
it("returns default task name when no profile is set", () => {
|
||||
const result = resolveGatewayWindowsTaskName();
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
expect(result).toBe("Moltbot Gateway");
|
||||
expect(result).toBe("OpenClaw Gateway");
|
||||
});
|
||||
|
||||
it("returns default task name when profile is undefined", () => {
|
||||
@@ -130,17 +130,17 @@ describe("resolveGatewayWindowsTaskName", () => {
|
||||
|
||||
it("returns profile-specific task name when profile is set", () => {
|
||||
const result = resolveGatewayWindowsTaskName("dev");
|
||||
expect(result).toBe("Moltbot Gateway (dev)");
|
||||
expect(result).toBe("OpenClaw Gateway (dev)");
|
||||
});
|
||||
|
||||
it("returns profile-specific task name for custom profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName("work");
|
||||
expect(result).toBe("Moltbot Gateway (work)");
|
||||
expect(result).toBe("OpenClaw Gateway (work)");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName(" ci ");
|
||||
expect(result).toBe("Moltbot Gateway (ci)");
|
||||
expect(result).toBe("OpenClaw Gateway (ci)");
|
||||
});
|
||||
|
||||
it("returns default task name for empty string profile", () => {
|
||||
@@ -175,24 +175,24 @@ describe("resolveGatewayProfileSuffix", () => {
|
||||
|
||||
describe("formatGatewayServiceDescription", () => {
|
||||
it("returns default description when no profile/version", () => {
|
||||
expect(formatGatewayServiceDescription()).toBe("Moltbot Gateway");
|
||||
expect(formatGatewayServiceDescription()).toBe("OpenClaw Gateway");
|
||||
});
|
||||
|
||||
it("includes profile when set", () => {
|
||||
expect(formatGatewayServiceDescription({ profile: "work" })).toBe(
|
||||
"Moltbot Gateway (profile: work)",
|
||||
"OpenClaw Gateway (profile: work)",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes version when set", () => {
|
||||
expect(formatGatewayServiceDescription({ version: "2026.1.10" })).toBe(
|
||||
"Moltbot Gateway (v2026.1.10)",
|
||||
"OpenClaw Gateway (v2026.1.10)",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes profile and version when set", () => {
|
||||
expect(formatGatewayServiceDescription({ profile: "dev", version: "1.2.3" })).toBe(
|
||||
"Moltbot Gateway (profile: dev, v1.2.3)",
|
||||
"OpenClaw Gateway (profile: dev, v1.2.3)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
// Default service labels (for backward compatibility and when no profile specified)
|
||||
export const GATEWAY_LAUNCH_AGENT_LABEL = "bot.molt.gateway";
|
||||
export const GATEWAY_SYSTEMD_SERVICE_NAME = "moltbot-gateway";
|
||||
export const GATEWAY_WINDOWS_TASK_NAME = "Moltbot Gateway";
|
||||
export const GATEWAY_SERVICE_MARKER = "moltbot";
|
||||
// Default service labels (canonical + legacy compatibility)
|
||||
export const GATEWAY_LAUNCH_AGENT_LABEL = "ai.openclaw.gateway";
|
||||
export const GATEWAY_SYSTEMD_SERVICE_NAME = "openclaw-gateway";
|
||||
export const GATEWAY_WINDOWS_TASK_NAME = "OpenClaw Gateway";
|
||||
export const GATEWAY_SERVICE_MARKER = "openclaw";
|
||||
export const GATEWAY_SERVICE_KIND = "gateway";
|
||||
export const NODE_LAUNCH_AGENT_LABEL = "bot.molt.node";
|
||||
export const NODE_SYSTEMD_SERVICE_NAME = "moltbot-node";
|
||||
export const NODE_WINDOWS_TASK_NAME = "Moltbot Node";
|
||||
export const NODE_SERVICE_MARKER = "moltbot";
|
||||
export const NODE_LAUNCH_AGENT_LABEL = "ai.openclaw.node";
|
||||
export const NODE_SYSTEMD_SERVICE_NAME = "openclaw-node";
|
||||
export const NODE_WINDOWS_TASK_NAME = "OpenClaw Node";
|
||||
export const NODE_SERVICE_MARKER = "openclaw";
|
||||
export const NODE_SERVICE_KIND = "node";
|
||||
export const NODE_WINDOWS_TASK_SCRIPT_NAME = "node.cmd";
|
||||
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = [
|
||||
"com.clawdbot.gateway",
|
||||
"com.steipete.clawdbot.gateway",
|
||||
];
|
||||
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS: string[] = [];
|
||||
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES: string[] = [];
|
||||
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
|
||||
|
||||
@@ -33,27 +30,24 @@ export function resolveGatewayLaunchAgentLabel(profile?: string): string {
|
||||
if (!normalized) {
|
||||
return GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
}
|
||||
return `bot.molt.${normalized}`;
|
||||
return `ai.openclaw.${normalized}`;
|
||||
}
|
||||
|
||||
export function resolveLegacyGatewayLaunchAgentLabels(profile?: string): string[] {
|
||||
const normalized = normalizeGatewayProfile(profile);
|
||||
if (!normalized) {
|
||||
return [...LEGACY_GATEWAY_LAUNCH_AGENT_LABELS];
|
||||
}
|
||||
return [...LEGACY_GATEWAY_LAUNCH_AGENT_LABELS, `com.clawdbot.${normalized}`];
|
||||
void profile;
|
||||
return [];
|
||||
}
|
||||
|
||||
export function resolveGatewaySystemdServiceName(profile?: string): string {
|
||||
const suffix = resolveGatewayProfileSuffix(profile);
|
||||
if (!suffix) return GATEWAY_SYSTEMD_SERVICE_NAME;
|
||||
return `moltbot-gateway${suffix}`;
|
||||
return `openclaw-gateway${suffix}`;
|
||||
}
|
||||
|
||||
export function resolveGatewayWindowsTaskName(profile?: string): string {
|
||||
const normalized = normalizeGatewayProfile(profile);
|
||||
if (!normalized) return GATEWAY_WINDOWS_TASK_NAME;
|
||||
return `Moltbot Gateway (${normalized})`;
|
||||
return `OpenClaw Gateway (${normalized})`;
|
||||
}
|
||||
|
||||
export function formatGatewayServiceDescription(params?: {
|
||||
@@ -65,8 +59,8 @@ export function formatGatewayServiceDescription(params?: {
|
||||
const parts: string[] = [];
|
||||
if (profile) parts.push(`profile: ${profile}`);
|
||||
if (version) parts.push(`v${version}`);
|
||||
if (parts.length === 0) return "Moltbot Gateway";
|
||||
return `Moltbot Gateway (${parts.join(", ")})`;
|
||||
if (parts.length === 0) return "OpenClaw Gateway";
|
||||
return `OpenClaw Gateway (${parts.join(", ")})`;
|
||||
}
|
||||
|
||||
export function resolveNodeLaunchAgentLabel(): string {
|
||||
@@ -83,6 +77,6 @@ export function resolveNodeWindowsTaskName(): string {
|
||||
|
||||
export function formatNodeServiceDescription(params?: { version?: string }): string {
|
||||
const version = params?.version?.trim();
|
||||
if (!version) return "Moltbot Node Host";
|
||||
return `Moltbot Node Host (v${version})`;
|
||||
if (!version) return "OpenClaw Node Host";
|
||||
return `OpenClaw Node Host (v${version})`;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ import { promisify } from "node:util";
|
||||
import {
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||
LEGACY_GATEWAY_WINDOWS_TASK_NAMES,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
resolveLegacyGatewayLaunchAgentLabels,
|
||||
} from "./constants.js";
|
||||
|
||||
export type ExtraGatewayService = {
|
||||
@@ -25,13 +22,13 @@ export type FindExtraGatewayServicesOptions = {
|
||||
deep?: boolean;
|
||||
};
|
||||
|
||||
const EXTRA_MARKERS = ["moltbot"];
|
||||
const EXTRA_MARKERS = ["openclaw"];
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export function renderGatewayServiceCleanupHints(
|
||||
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
||||
): string[] {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
const profile = env.OPENCLAW_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin": {
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
@@ -66,32 +63,38 @@ function containsMarker(content: string): boolean {
|
||||
|
||||
function hasGatewayServiceMarker(content: string): boolean {
|
||||
const lower = content.toLowerCase();
|
||||
const markerKeys = ["openclaw_service_marker"];
|
||||
const kindKeys = ["openclaw_service_kind"];
|
||||
const markerValues = [GATEWAY_SERVICE_MARKER.toLowerCase()];
|
||||
const hasMarkerKey = markerKeys.some((key) => lower.includes(key));
|
||||
const hasKindKey = kindKeys.some((key) => lower.includes(key));
|
||||
const hasMarkerValue = markerValues.some((value) => lower.includes(value));
|
||||
return (
|
||||
lower.includes("moltbot_service_marker") &&
|
||||
lower.includes(GATEWAY_SERVICE_MARKER.toLowerCase()) &&
|
||||
lower.includes("moltbot_service_kind") &&
|
||||
hasMarkerKey &&
|
||||
hasKindKey &&
|
||||
hasMarkerValue &&
|
||||
lower.includes(GATEWAY_SERVICE_KIND.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
function isMoltbotGatewayLaunchdService(label: string, contents: string): boolean {
|
||||
function isOpenClawGatewayLaunchdService(label: string, contents: string): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
const lowerContents = contents.toLowerCase();
|
||||
if (!lowerContents.includes("gateway")) return false;
|
||||
return label.startsWith("bot.molt.") || label.startsWith("com.clawdbot.");
|
||||
return label.startsWith("ai.openclaw.");
|
||||
}
|
||||
|
||||
function isMoltbotGatewaySystemdService(name: string, contents: string): boolean {
|
||||
function isOpenClawGatewaySystemdService(name: string, contents: string): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
if (!name.startsWith("moltbot-gateway")) return false;
|
||||
if (!name.startsWith("openclaw-gateway")) return false;
|
||||
return contents.toLowerCase().includes("gateway");
|
||||
}
|
||||
|
||||
function isMoltbotGatewayTaskName(name: string): boolean {
|
||||
function isOpenClawGatewayTaskName(name: string): boolean {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
const defaultName = resolveGatewayWindowsTaskName().toLowerCase();
|
||||
return normalized === defaultName || normalized.startsWith("moltbot gateway");
|
||||
return normalized === defaultName || normalized.startsWith("openclaw gateway");
|
||||
}
|
||||
|
||||
function tryExtractPlistLabel(contents: string): string | null {
|
||||
@@ -101,17 +104,11 @@ function tryExtractPlistLabel(contents: string): string | null {
|
||||
}
|
||||
|
||||
function isIgnoredLaunchdLabel(label: string): boolean {
|
||||
return (
|
||||
label === resolveGatewayLaunchAgentLabel() ||
|
||||
resolveLegacyGatewayLaunchAgentLabels(process.env.CLAWDBOT_PROFILE).includes(label)
|
||||
);
|
||||
return label === resolveGatewayLaunchAgentLabel();
|
||||
}
|
||||
|
||||
function isIgnoredSystemdName(name: string): boolean {
|
||||
return (
|
||||
name === resolveGatewaySystemdServiceName() ||
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES.includes(name)
|
||||
);
|
||||
return name === resolveGatewaySystemdServiceName();
|
||||
}
|
||||
|
||||
async function scanLaunchdDir(params: {
|
||||
@@ -140,7 +137,7 @@ async function scanLaunchdDir(params: {
|
||||
if (!containsMarker(contents)) continue;
|
||||
const label = tryExtractPlistLabel(contents) ?? labelFromName;
|
||||
if (isIgnoredLaunchdLabel(label)) continue;
|
||||
if (isMoltbotGatewayLaunchdService(label, contents)) continue;
|
||||
if (isOpenClawGatewayLaunchdService(label, contents)) continue;
|
||||
results.push({
|
||||
platform: "darwin",
|
||||
label,
|
||||
@@ -176,7 +173,7 @@ async function scanSystemdDir(params: {
|
||||
continue;
|
||||
}
|
||||
if (!containsMarker(contents)) continue;
|
||||
if (isMoltbotGatewaySystemdService(name, contents)) continue;
|
||||
if (isOpenClawGatewaySystemdService(name, contents)) continue;
|
||||
results.push({
|
||||
platform: "linux",
|
||||
label: entry,
|
||||
@@ -336,8 +333,7 @@ export async function findExtraGatewayServices(
|
||||
for (const task of tasks) {
|
||||
const name = task.name.trim();
|
||||
if (!name) continue;
|
||||
if (isMoltbotGatewayTaskName(name)) continue;
|
||||
if (LEGACY_GATEWAY_WINDOWS_TASK_NAMES.includes(name)) continue;
|
||||
if (isOpenClawGatewayTaskName(name)) continue;
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
|
||||
const matches = EXTRA_MARKERS.some(
|
||||
|
||||
@@ -18,10 +18,10 @@ async function withLaunchctlStub(
|
||||
run: (context: { env: Record<string, string | undefined>; logPath: string }) => Promise<void>,
|
||||
) {
|
||||
const originalPath = process.env.PATH;
|
||||
const originalLogPath = process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG;
|
||||
const originalListOutput = process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT;
|
||||
const originalLogPath = process.env.OPENCLAW_TEST_LAUNCHCTL_LOG;
|
||||
const originalListOutput = process.env.OPENCLAW_TEST_LAUNCHCTL_LIST_OUTPUT;
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-launchctl-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-launchctl-test-"));
|
||||
try {
|
||||
const binDir = path.join(tmpDir, "bin");
|
||||
const homeDir = path.join(tmpDir, "home");
|
||||
@@ -35,12 +35,12 @@ async function withLaunchctlStub(
|
||||
[
|
||||
'import fs from "node:fs";',
|
||||
"const args = process.argv.slice(2);",
|
||||
"const logPath = process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG;",
|
||||
"const logPath = process.env.OPENCLAW_TEST_LAUNCHCTL_LOG;",
|
||||
"if (logPath) {",
|
||||
' fs.appendFileSync(logPath, JSON.stringify(args) + "\\n", "utf8");',
|
||||
"}",
|
||||
'if (args[0] === "list") {',
|
||||
' const output = process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT || "";',
|
||||
' const output = process.env.OPENCLAW_TEST_LAUNCHCTL_LIST_OUTPUT || "";',
|
||||
" process.stdout.write(output);",
|
||||
"}",
|
||||
"process.exit(0);",
|
||||
@@ -61,28 +61,28 @@ async function withLaunchctlStub(
|
||||
await fs.chmod(shPath, 0o755);
|
||||
}
|
||||
|
||||
process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG = logPath;
|
||||
process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT = options.listOutput ?? "";
|
||||
process.env.OPENCLAW_TEST_LAUNCHCTL_LOG = logPath;
|
||||
process.env.OPENCLAW_TEST_LAUNCHCTL_LIST_OUTPUT = options.listOutput ?? "";
|
||||
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
||||
|
||||
await run({
|
||||
env: {
|
||||
HOME: homeDir,
|
||||
CLAWDBOT_PROFILE: "default",
|
||||
OPENCLAW_PROFILE: "default",
|
||||
},
|
||||
logPath,
|
||||
});
|
||||
} finally {
|
||||
process.env.PATH = originalPath;
|
||||
if (originalLogPath === undefined) {
|
||||
delete process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG;
|
||||
delete process.env.OPENCLAW_TEST_LAUNCHCTL_LOG;
|
||||
} else {
|
||||
process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG = originalLogPath;
|
||||
process.env.OPENCLAW_TEST_LAUNCHCTL_LOG = originalLogPath;
|
||||
}
|
||||
if (originalListOutput === undefined) {
|
||||
delete process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT;
|
||||
delete process.env.OPENCLAW_TEST_LAUNCHCTL_LIST_OUTPUT;
|
||||
} else {
|
||||
process.env.CLAWDBOT_TEST_LAUNCHCTL_LIST_OUTPUT = originalListOutput;
|
||||
process.env.OPENCLAW_TEST_LAUNCHCTL_LIST_OUTPUT = originalListOutput;
|
||||
}
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -107,7 +107,7 @@ describe("launchd runtime parsing", () => {
|
||||
|
||||
describe("launchctl list detection", () => {
|
||||
it("detects the resolved label in launchctl list", async () => {
|
||||
await withLaunchctlStub({ listOutput: "123 0 bot.molt.gateway\n" }, async ({ env }) => {
|
||||
await withLaunchctlStub({ listOutput: "123 0 ai.openclaw.gateway\n" }, async ({ env }) => {
|
||||
const listed = await isLaunchAgentListed({ env });
|
||||
expect(listed).toBe(true);
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("launchd bootstrap repair", () => {
|
||||
.map((line) => JSON.parse(line) as string[]);
|
||||
|
||||
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
|
||||
const label = "bot.molt.gateway";
|
||||
const label = "ai.openclaw.gateway";
|
||||
const plistPath = resolveLaunchAgentPlistPath(env);
|
||||
|
||||
expect(calls).toContainEqual(["bootstrap", domain, plistPath]);
|
||||
@@ -145,9 +145,9 @@ describe("launchd bootstrap repair", () => {
|
||||
describe("launchd install", () => {
|
||||
it("enables service before bootstrap (clears persisted disabled state)", async () => {
|
||||
const originalPath = process.env.PATH;
|
||||
const originalLogPath = process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG;
|
||||
const originalLogPath = process.env.OPENCLAW_TEST_LAUNCHCTL_LOG;
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-launchctl-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-launchctl-test-"));
|
||||
try {
|
||||
const binDir = path.join(tmpDir, "bin");
|
||||
const homeDir = path.join(tmpDir, "home");
|
||||
@@ -160,7 +160,7 @@ describe("launchd install", () => {
|
||||
stubJsPath,
|
||||
[
|
||||
'import fs from "node:fs";',
|
||||
"const logPath = process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG;",
|
||||
"const logPath = process.env.OPENCLAW_TEST_LAUNCHCTL_LOG;",
|
||||
"if (logPath) {",
|
||||
' fs.appendFileSync(logPath, JSON.stringify(process.argv.slice(2)) + "\\n", "utf8");',
|
||||
"}",
|
||||
@@ -182,12 +182,12 @@ describe("launchd install", () => {
|
||||
await fs.chmod(shPath, 0o755);
|
||||
}
|
||||
|
||||
process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG = logPath;
|
||||
process.env.OPENCLAW_TEST_LAUNCHCTL_LOG = logPath;
|
||||
process.env.PATH = `${binDir}${path.delimiter}${originalPath ?? ""}`;
|
||||
|
||||
const env: Record<string, string | undefined> = {
|
||||
HOME: homeDir,
|
||||
CLAWDBOT_PROFILE: "default",
|
||||
OPENCLAW_PROFILE: "default",
|
||||
};
|
||||
await installLaunchAgent({
|
||||
env,
|
||||
@@ -201,7 +201,7 @@ describe("launchd install", () => {
|
||||
.map((line) => JSON.parse(line) as string[]);
|
||||
|
||||
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
|
||||
const label = "bot.molt.gateway";
|
||||
const label = "ai.openclaw.gateway";
|
||||
const plistPath = resolveLaunchAgentPlistPath(env);
|
||||
const serviceId = `${domain}/${label}`;
|
||||
|
||||
@@ -218,9 +218,9 @@ describe("launchd install", () => {
|
||||
} finally {
|
||||
process.env.PATH = originalPath;
|
||||
if (originalLogPath === undefined) {
|
||||
delete process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG;
|
||||
delete process.env.OPENCLAW_TEST_LAUNCHCTL_LOG;
|
||||
} else {
|
||||
process.env.CLAWDBOT_TEST_LAUNCHCTL_LOG = originalLogPath;
|
||||
process.env.OPENCLAW_TEST_LAUNCHCTL_LOG = originalLogPath;
|
||||
}
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -228,77 +228,77 @@ describe("launchd install", () => {
|
||||
});
|
||||
|
||||
describe("resolveLaunchAgentPlistPath", () => {
|
||||
it("uses default label when CLAWDBOT_PROFILE is default", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "default" };
|
||||
it("uses default label when OPENCLAW_PROFILE is default", () => {
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "default" };
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.gateway.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses default label when CLAWDBOT_PROFILE is unset", () => {
|
||||
it("uses default label when OPENCLAW_PROFILE is unset", () => {
|
||||
const env = { HOME: "/Users/test" };
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.gateway.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses profile-specific label when CLAWDBOT_PROFILE is set to a custom value", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "jbphoenix" };
|
||||
it("uses profile-specific label when OPENCLAW_PROFILE is set to a custom value", () => {
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "jbphoenix" };
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.jbphoenix.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.jbphoenix.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers CLAWDBOT_LAUNCHD_LABEL over CLAWDBOT_PROFILE", () => {
|
||||
it("prefers OPENCLAW_LAUNCHD_LABEL over OPENCLAW_PROFILE", () => {
|
||||
const env = {
|
||||
HOME: "/Users/test",
|
||||
CLAWDBOT_PROFILE: "jbphoenix",
|
||||
CLAWDBOT_LAUNCHD_LABEL: "com.custom.label",
|
||||
OPENCLAW_PROFILE: "jbphoenix",
|
||||
OPENCLAW_LAUNCHD_LABEL: "com.custom.label",
|
||||
};
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/com.custom.label.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from CLAWDBOT_LAUNCHD_LABEL", () => {
|
||||
it("trims whitespace from OPENCLAW_LAUNCHD_LABEL", () => {
|
||||
const env = {
|
||||
HOME: "/Users/test",
|
||||
CLAWDBOT_LAUNCHD_LABEL: " com.custom.label ",
|
||||
OPENCLAW_LAUNCHD_LABEL: " com.custom.label ",
|
||||
};
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/com.custom.label.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores empty CLAWDBOT_LAUNCHD_LABEL and falls back to profile", () => {
|
||||
it("ignores empty OPENCLAW_LAUNCHD_LABEL and falls back to profile", () => {
|
||||
const env = {
|
||||
HOME: "/Users/test",
|
||||
CLAWDBOT_PROFILE: "myprofile",
|
||||
CLAWDBOT_LAUNCHD_LABEL: " ",
|
||||
OPENCLAW_PROFILE: "myprofile",
|
||||
OPENCLAW_LAUNCHD_LABEL: " ",
|
||||
};
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.myprofile.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.myprofile.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'Default' profile", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "Default" };
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "Default" };
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.gateway.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'DEFAULT' profile", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "DEFAULT" };
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "DEFAULT" };
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.gateway.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.gateway.plist",
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from CLAWDBOT_PROFILE", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: " myprofile " };
|
||||
it("trims whitespace from OPENCLAW_PROFILE", () => {
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: " myprofile " };
|
||||
expect(resolveLaunchAgentPlistPath(env)).toBe(
|
||||
"/Users/test/Library/LaunchAgents/bot.molt.myprofile.plist",
|
||||
"/Users/test/Library/LaunchAgents/ai.openclaw.myprofile.plist",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,9 +27,9 @@ const formatLine = (label: string, value: string) => {
|
||||
};
|
||||
|
||||
function resolveLaunchAgentLabel(args?: { env?: Record<string, string | undefined> }): string {
|
||||
const envLabel = args?.env?.CLAWDBOT_LAUNCHD_LABEL?.trim();
|
||||
const envLabel = args?.env?.OPENCLAW_LAUNCHD_LABEL?.trim();
|
||||
if (envLabel) return envLabel;
|
||||
return resolveGatewayLaunchAgentLabel(args?.env?.CLAWDBOT_PROFILE);
|
||||
return resolveGatewayLaunchAgentLabel(args?.env?.OPENCLAW_PROFILE);
|
||||
}
|
||||
|
||||
function resolveLaunchAgentPlistPathForLabel(
|
||||
@@ -52,7 +52,7 @@ export function resolveGatewayLogPaths(env: Record<string, string | undefined>):
|
||||
} {
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
const logDir = path.join(stateDir, "logs");
|
||||
const prefix = env.CLAWDBOT_LOG_PREFIX?.trim() || "gateway";
|
||||
const prefix = env.OPENCLAW_LOG_PREFIX?.trim() || "gateway";
|
||||
return {
|
||||
logDir,
|
||||
stdoutPath: path.join(logDir, `${prefix}.log`),
|
||||
@@ -248,7 +248,7 @@ export async function findLegacyLaunchAgents(
|
||||
): Promise<LegacyLaunchAgent[]> {
|
||||
const domain = resolveGuiDomain();
|
||||
const results: LegacyLaunchAgent[] = [];
|
||||
for (const label of resolveLegacyGatewayLaunchAgentLabels(env.CLAWDBOT_PROFILE)) {
|
||||
for (const label of resolveLegacyGatewayLaunchAgentLabels(env.OPENCLAW_PROFILE)) {
|
||||
const plistPath = resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
const res = await execLaunchctl(["print", `${domain}/${label}`]);
|
||||
const loaded = res.code === 0;
|
||||
@@ -384,7 +384,7 @@ export async function installLaunchAgent({
|
||||
|
||||
const domain = resolveGuiDomain();
|
||||
const label = resolveLaunchAgentLabel({ env });
|
||||
for (const legacyLabel of resolveLegacyGatewayLaunchAgentLabels(env.CLAWDBOT_PROFILE)) {
|
||||
for (const legacyLabel of resolveLegacyGatewayLaunchAgentLabels(env.OPENCLAW_PROFILE)) {
|
||||
const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(env, legacyLabel);
|
||||
await execLaunchctl(["bootout", domain, legacyPlistPath]);
|
||||
await execLaunchctl(["unload", legacyPlistPath]);
|
||||
@@ -401,8 +401,8 @@ export async function installLaunchAgent({
|
||||
const serviceDescription =
|
||||
description ??
|
||||
formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version: environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
profile: env.OPENCLAW_PROFILE,
|
||||
version: environment?.OPENCLAW_SERVICE_VERSION ?? env.OPENCLAW_SERVICE_VERSION,
|
||||
});
|
||||
const plist = buildLaunchAgentPlist({
|
||||
label,
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { findLegacyLaunchAgents, uninstallLegacyLaunchAgents } from "./launchd.js";
|
||||
import { findLegacySystemdUnits, uninstallLegacySystemdUnits } from "./systemd.js";
|
||||
|
||||
export type LegacyGatewayService = {
|
||||
platform: "darwin" | "linux" | "win32";
|
||||
label: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
function formatLegacyLaunchAgents(
|
||||
agents: Awaited<ReturnType<typeof findLegacyLaunchAgents>>,
|
||||
): LegacyGatewayService[] {
|
||||
return agents.map((agent) => ({
|
||||
platform: "darwin",
|
||||
label: agent.label,
|
||||
detail: [
|
||||
agent.loaded ? "loaded" : "not loaded",
|
||||
agent.exists ? `plist: ${agent.plistPath}` : "plist missing",
|
||||
].join(", "),
|
||||
}));
|
||||
}
|
||||
|
||||
function formatLegacySystemdUnits(
|
||||
units: Awaited<ReturnType<typeof findLegacySystemdUnits>>,
|
||||
): LegacyGatewayService[] {
|
||||
return units.map((unit) => ({
|
||||
platform: "linux",
|
||||
label: `${unit.name}.service`,
|
||||
detail: [
|
||||
unit.enabled ? "enabled" : "disabled",
|
||||
unit.exists ? `unit: ${unit.unitPath}` : "unit missing",
|
||||
].join(", "),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function findLegacyGatewayServices(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<LegacyGatewayService[]> {
|
||||
if (process.platform === "darwin") {
|
||||
const agents = await findLegacyLaunchAgents(env);
|
||||
return formatLegacyLaunchAgents(agents);
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
const units = await findLegacySystemdUnits(env);
|
||||
return formatLegacySystemdUnits(units);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function uninstallLegacyGatewayServices({
|
||||
env,
|
||||
stdout,
|
||||
}: {
|
||||
env: Record<string, string | undefined>;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}): Promise<LegacyGatewayService[]> {
|
||||
if (process.platform === "darwin") {
|
||||
const agents = await uninstallLegacyLaunchAgents({ env, stdout });
|
||||
return formatLegacyLaunchAgents(agents);
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
const units = await uninstallLegacySystemdUnits({ env, stdout });
|
||||
return formatLegacySystemdUnits(units);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -14,13 +14,13 @@ function withNodeServiceEnv(
|
||||
): Record<string, string | undefined> {
|
||||
return {
|
||||
...env,
|
||||
CLAWDBOT_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(),
|
||||
CLAWDBOT_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
|
||||
CLAWDBOT_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(),
|
||||
CLAWDBOT_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME,
|
||||
CLAWDBOT_LOG_PREFIX: "node",
|
||||
CLAWDBOT_SERVICE_MARKER: NODE_SERVICE_MARKER,
|
||||
CLAWDBOT_SERVICE_KIND: NODE_SERVICE_KIND,
|
||||
OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(),
|
||||
OPENCLAW_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
|
||||
OPENCLAW_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(),
|
||||
OPENCLAW_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME,
|
||||
OPENCLAW_LOG_PREFIX: "node",
|
||||
OPENCLAW_SERVICE_MARKER: NODE_SERVICE_MARKER,
|
||||
OPENCLAW_SERVICE_KIND: NODE_SERVICE_KIND,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ function withNodeInstallEnv(args: GatewayServiceInstallArgs): GatewayServiceInst
|
||||
env: withNodeServiceEnv(args.env),
|
||||
environment: {
|
||||
...args.environment,
|
||||
CLAWDBOT_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(),
|
||||
CLAWDBOT_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
|
||||
CLAWDBOT_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(),
|
||||
CLAWDBOT_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME,
|
||||
CLAWDBOT_LOG_PREFIX: "node",
|
||||
CLAWDBOT_SERVICE_MARKER: NODE_SERVICE_MARKER,
|
||||
CLAWDBOT_SERVICE_KIND: NODE_SERVICE_KIND,
|
||||
OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(),
|
||||
OPENCLAW_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
|
||||
OPENCLAW_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(),
|
||||
OPENCLAW_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME,
|
||||
OPENCLAW_LOG_PREFIX: "node",
|
||||
OPENCLAW_SERVICE_MARKER: NODE_SERVICE_MARKER,
|
||||
OPENCLAW_SERVICE_KIND: NODE_SERVICE_KIND,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,31 +7,31 @@ import { resolveGatewayStateDir } from "./paths.js";
|
||||
describe("resolveGatewayStateDir", () => {
|
||||
it("uses the default state dir when no overrides are set", () => {
|
||||
const env = { HOME: "/Users/test" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".clawdbot"));
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".openclaw"));
|
||||
});
|
||||
|
||||
it("appends the profile suffix when set", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "rescue" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".clawdbot-rescue"));
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "rescue" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".openclaw-rescue"));
|
||||
});
|
||||
|
||||
it("treats default profiles as the base state dir", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_PROFILE: "Default" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".clawdbot"));
|
||||
const env = { HOME: "/Users/test", OPENCLAW_PROFILE: "Default" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.join("/Users/test", ".openclaw"));
|
||||
});
|
||||
|
||||
it("uses CLAWDBOT_STATE_DIR when provided", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_STATE_DIR: "/var/lib/moltbot" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.resolve("/var/lib/moltbot"));
|
||||
it("uses OPENCLAW_STATE_DIR when provided", () => {
|
||||
const env = { HOME: "/Users/test", OPENCLAW_STATE_DIR: "/var/lib/openclaw" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.resolve("/var/lib/openclaw"));
|
||||
});
|
||||
|
||||
it("expands ~ in CLAWDBOT_STATE_DIR", () => {
|
||||
const env = { HOME: "/Users/test", CLAWDBOT_STATE_DIR: "~/moltbot-state" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.resolve("/Users/test/moltbot-state"));
|
||||
it("expands ~ in OPENCLAW_STATE_DIR", () => {
|
||||
const env = { HOME: "/Users/test", OPENCLAW_STATE_DIR: "~/openclaw-state" };
|
||||
expect(resolveGatewayStateDir(env)).toBe(path.resolve("/Users/test/openclaw-state"));
|
||||
});
|
||||
|
||||
it("preserves Windows absolute paths without HOME", () => {
|
||||
const env = { CLAWDBOT_STATE_DIR: "C:\\State\\moltbot" };
|
||||
expect(resolveGatewayStateDir(env)).toBe("C:\\State\\moltbot");
|
||||
const env = { OPENCLAW_STATE_DIR: "C:\\State\\openclaw" };
|
||||
expect(resolveGatewayStateDir(env)).toBe("C:\\State\\openclaw");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,12 +26,12 @@ export function resolveUserPathWithHome(input: string, home?: string): string {
|
||||
}
|
||||
|
||||
export function resolveGatewayStateDir(env: Record<string, string | undefined>): string {
|
||||
const override = env.CLAWDBOT_STATE_DIR?.trim();
|
||||
const override = env.OPENCLAW_STATE_DIR?.trim();
|
||||
if (override) {
|
||||
const home = override.startsWith("~") ? resolveHomeDir(env) : undefined;
|
||||
return resolveUserPathWithHome(override, home);
|
||||
}
|
||||
const home = resolveHomeDir(env);
|
||||
const suffix = resolveGatewayProfileSuffix(env.CLAWDBOT_PROFILE);
|
||||
return path.join(home, `.clawdbot${suffix}`);
|
||||
const suffix = resolveGatewayProfileSuffix(env.OPENCLAW_PROFILE);
|
||||
return path.join(home, `.openclaw${suffix}`);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ afterEach(() => {
|
||||
|
||||
describe("resolveGatewayProgramArguments", () => {
|
||||
it("uses realpath-resolved dist entry when running via npx shim", async () => {
|
||||
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/moltbot");
|
||||
const entryPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/moltbot/dist/entry.js");
|
||||
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/openclaw");
|
||||
const entryPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/openclaw/dist/entry.js");
|
||||
process.argv = ["node", argv1];
|
||||
fsMocks.realpath.mockResolvedValue(entryPath);
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
@@ -46,13 +46,13 @@ describe("resolveGatewayProgramArguments", () => {
|
||||
});
|
||||
|
||||
it("prefers symlinked path over realpath for stable service config", async () => {
|
||||
// Simulates pnpm global install where node_modules/moltbot is a symlink
|
||||
// to .pnpm/moltbot@X.Y.Z/node_modules/moltbot
|
||||
// Simulates pnpm global install where node_modules/openclaw is a symlink
|
||||
// to .pnpm/openclaw@X.Y.Z/node_modules/openclaw
|
||||
const symlinkPath = path.resolve(
|
||||
"/Users/test/Library/pnpm/global/5/node_modules/moltbot/dist/entry.js",
|
||||
"/Users/test/Library/pnpm/global/5/node_modules/openclaw/dist/entry.js",
|
||||
);
|
||||
const realpathResolved = path.resolve(
|
||||
"/Users/test/Library/pnpm/global/5/node_modules/.pnpm/moltbot@2026.1.21-2/node_modules/moltbot/dist/entry.js",
|
||||
"/Users/test/Library/pnpm/global/5/node_modules/.pnpm/openclaw@2026.1.21-2/node_modules/openclaw/dist/entry.js",
|
||||
);
|
||||
process.argv = ["node", symlinkPath];
|
||||
fsMocks.realpath.mockResolvedValue(realpathResolved);
|
||||
@@ -66,8 +66,8 @@ describe("resolveGatewayProgramArguments", () => {
|
||||
});
|
||||
|
||||
it("falls back to node_modules package dist when .bin path is not resolved", async () => {
|
||||
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/moltbot");
|
||||
const indexPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/moltbot/dist/index.js");
|
||||
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/openclaw");
|
||||
const indexPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/openclaw/dist/index.js");
|
||||
process.argv = ["node", argv1];
|
||||
fsMocks.realpath.mockRejectedValue(new Error("no realpath"));
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ async function resolveCliEntrypointPathForService(): Promise<string> {
|
||||
await fs.access(resolvedPath);
|
||||
// Prefer the original (possibly symlinked) path over the resolved realpath.
|
||||
// This keeps LaunchAgent/systemd paths stable across package version updates,
|
||||
// since symlinks like node_modules/moltbot -> .pnpm/moltbot@X.Y.Z/...
|
||||
// since symlinks like node_modules/openclaw -> .pnpm/openclaw@X.Y.Z/...
|
||||
// are automatically updated by pnpm, while the resolved path contains
|
||||
// version-specific directories that break after updates.
|
||||
const normalizedLooksLikeDist = /[/\\]dist[/\\].+\.(cjs|js|mjs)$/.test(normalized);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { parseSchtasksQuery, readScheduledTaskCommand, resolveTaskScriptPath } f
|
||||
describe("schtasks runtime parsing", () => {
|
||||
it("parses status and last run info", () => {
|
||||
const output = [
|
||||
"TaskName: \\Moltbot Gateway",
|
||||
"TaskName: \\OpenClaw Gateway",
|
||||
"Status: Ready",
|
||||
"Last Run Time: 1/8/2026 1:23:45 AM",
|
||||
"Last Run Result: 0x0",
|
||||
@@ -23,7 +23,7 @@ describe("schtasks runtime parsing", () => {
|
||||
|
||||
it("parses running status", () => {
|
||||
const output = [
|
||||
"TaskName: \\Moltbot Gateway",
|
||||
"TaskName: \\OpenClaw Gateway",
|
||||
"Status: Running",
|
||||
"Last Run Time: 1/8/2026 1:23:45 AM",
|
||||
"Last Run Result: 0x0",
|
||||
@@ -37,68 +37,68 @@ describe("schtasks runtime parsing", () => {
|
||||
});
|
||||
|
||||
describe("resolveTaskScriptPath", () => {
|
||||
it("uses default path when CLAWDBOT_PROFILE is default", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "default" };
|
||||
it("uses default path when OPENCLAW_PROFILE is default", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: "default" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
path.join("C:\\Users\\test", ".clawdbot", "gateway.cmd"),
|
||||
path.join("C:\\Users\\test", ".openclaw", "gateway.cmd"),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses default path when CLAWDBOT_PROFILE is unset", () => {
|
||||
it("uses default path when OPENCLAW_PROFILE is unset", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
path.join("C:\\Users\\test", ".clawdbot", "gateway.cmd"),
|
||||
path.join("C:\\Users\\test", ".openclaw", "gateway.cmd"),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses profile-specific path when CLAWDBOT_PROFILE is set to a custom value", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "jbphoenix" };
|
||||
it("uses profile-specific path when OPENCLAW_PROFILE is set to a custom value", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: "jbphoenix" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
path.join("C:\\Users\\test", ".clawdbot-jbphoenix", "gateway.cmd"),
|
||||
path.join("C:\\Users\\test", ".openclaw-jbphoenix", "gateway.cmd"),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers CLAWDBOT_STATE_DIR over profile-derived defaults", () => {
|
||||
it("prefers OPENCLAW_STATE_DIR over profile-derived defaults", () => {
|
||||
const env = {
|
||||
USERPROFILE: "C:\\Users\\test",
|
||||
CLAWDBOT_PROFILE: "rescue",
|
||||
CLAWDBOT_STATE_DIR: "C:\\State\\moltbot",
|
||||
OPENCLAW_PROFILE: "rescue",
|
||||
OPENCLAW_STATE_DIR: "C:\\State\\openclaw",
|
||||
};
|
||||
expect(resolveTaskScriptPath(env)).toBe(path.join("C:\\State\\moltbot", "gateway.cmd"));
|
||||
expect(resolveTaskScriptPath(env)).toBe(path.join("C:\\State\\openclaw", "gateway.cmd"));
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'Default' profile", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "Default" };
|
||||
const env = { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: "Default" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
path.join("C:\\Users\\test", ".clawdbot", "gateway.cmd"),
|
||||
path.join("C:\\Users\\test", ".openclaw", "gateway.cmd"),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'DEFAULT' profile", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: "DEFAULT" };
|
||||
const env = { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: "DEFAULT" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
path.join("C:\\Users\\test", ".clawdbot", "gateway.cmd"),
|
||||
path.join("C:\\Users\\test", ".openclaw", "gateway.cmd"),
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from CLAWDBOT_PROFILE", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", CLAWDBOT_PROFILE: " myprofile " };
|
||||
it("trims whitespace from OPENCLAW_PROFILE", () => {
|
||||
const env = { USERPROFILE: "C:\\Users\\test", OPENCLAW_PROFILE: " myprofile " };
|
||||
expect(resolveTaskScriptPath(env)).toBe(
|
||||
path.join("C:\\Users\\test", ".clawdbot-myprofile", "gateway.cmd"),
|
||||
path.join("C:\\Users\\test", ".openclaw-myprofile", "gateway.cmd"),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to HOME when USERPROFILE is not set", () => {
|
||||
const env = { HOME: "/home/test", CLAWDBOT_PROFILE: "default" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(path.join("/home/test", ".clawdbot", "gateway.cmd"));
|
||||
const env = { HOME: "/home/test", OPENCLAW_PROFILE: "default" };
|
||||
expect(resolveTaskScriptPath(env)).toBe(path.join("/home/test", ".openclaw", "gateway.cmd"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("readScheduledTaskCommand", () => {
|
||||
it("parses basic command script", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmpDir, ".clawdbot", "gateway.cmd");
|
||||
const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd");
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
scriptPath,
|
||||
@@ -106,7 +106,7 @@ describe("readScheduledTaskCommand", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toEqual({
|
||||
programArguments: ["node", "gateway.js", "--port", "18789"],
|
||||
@@ -117,21 +117,21 @@ describe("readScheduledTaskCommand", () => {
|
||||
});
|
||||
|
||||
it("parses script with working directory", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmpDir, ".clawdbot", "gateway.cmd");
|
||||
const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd");
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
scriptPath,
|
||||
["@echo off", "cd /d C:\\Projects\\moltbot", "node gateway.js"].join("\r\n"),
|
||||
["@echo off", "cd /d C:\\Projects\\openclaw", "node gateway.js"].join("\r\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toEqual({
|
||||
programArguments: ["node", "gateway.js"],
|
||||
workingDirectory: "C:\\Projects\\moltbot",
|
||||
workingDirectory: "C:\\Projects\\openclaw",
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
@@ -139,9 +139,9 @@ describe("readScheduledTaskCommand", () => {
|
||||
});
|
||||
|
||||
it("parses script with environment variables", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmpDir, ".clawdbot", "gateway.cmd");
|
||||
const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd");
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
scriptPath,
|
||||
@@ -149,7 +149,7 @@ describe("readScheduledTaskCommand", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toEqual({
|
||||
programArguments: ["node", "gateway.js"],
|
||||
@@ -164,9 +164,9 @@ describe("readScheduledTaskCommand", () => {
|
||||
});
|
||||
|
||||
it("parses script with quoted arguments containing spaces", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmpDir, ".clawdbot", "gateway.cmd");
|
||||
const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd");
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
// Use forward slashes which work in Windows cmd and avoid escape parsing issues
|
||||
await fs.writeFile(
|
||||
@@ -175,7 +175,7 @@ describe("readScheduledTaskCommand", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toEqual({
|
||||
programArguments: ["C:/Program Files/Node/node.exe", "gateway.js"],
|
||||
@@ -186,9 +186,9 @@ describe("readScheduledTaskCommand", () => {
|
||||
});
|
||||
|
||||
it("returns null when script does not exist", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toBeNull();
|
||||
} finally {
|
||||
@@ -197,9 +197,9 @@ describe("readScheduledTaskCommand", () => {
|
||||
});
|
||||
|
||||
it("returns null when script has no command", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmpDir, ".clawdbot", "gateway.cmd");
|
||||
const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd");
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
scriptPath,
|
||||
@@ -207,7 +207,7 @@ describe("readScheduledTaskCommand", () => {
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toBeNull();
|
||||
} finally {
|
||||
@@ -216,31 +216,31 @@ describe("readScheduledTaskCommand", () => {
|
||||
});
|
||||
|
||||
it("parses full script with all components", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-schtasks-test-"));
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-test-"));
|
||||
try {
|
||||
const scriptPath = path.join(tmpDir, ".clawdbot", "gateway.cmd");
|
||||
const scriptPath = path.join(tmpDir, ".openclaw", "gateway.cmd");
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
scriptPath,
|
||||
[
|
||||
"@echo off",
|
||||
"rem Moltbot Gateway",
|
||||
"cd /d C:\\Projects\\moltbot",
|
||||
"rem OpenClaw Gateway",
|
||||
"cd /d C:\\Projects\\openclaw",
|
||||
"set NODE_ENV=production",
|
||||
"set CLAWDBOT_PORT=18789",
|
||||
"set OPENCLAW_PORT=18789",
|
||||
"node gateway.js --verbose",
|
||||
].join("\r\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const env = { USERPROFILE: tmpDir, CLAWDBOT_PROFILE: "default" };
|
||||
const env = { USERPROFILE: tmpDir, OPENCLAW_PROFILE: "default" };
|
||||
const result = await readScheduledTaskCommand(env);
|
||||
expect(result).toEqual({
|
||||
programArguments: ["node", "gateway.js", "--verbose"],
|
||||
workingDirectory: "C:\\Projects\\moltbot",
|
||||
workingDirectory: "C:\\Projects\\openclaw",
|
||||
environment: {
|
||||
NODE_ENV: "production",
|
||||
CLAWDBOT_PORT: "18789",
|
||||
OPENCLAW_PORT: "18789",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -17,15 +17,15 @@ const formatLine = (label: string, value: string) => {
|
||||
};
|
||||
|
||||
function resolveTaskName(env: Record<string, string | undefined>): string {
|
||||
const override = env.CLAWDBOT_WINDOWS_TASK_NAME?.trim();
|
||||
const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim();
|
||||
if (override) return override;
|
||||
return resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE);
|
||||
}
|
||||
|
||||
export function resolveTaskScriptPath(env: Record<string, string | undefined>): string {
|
||||
const override = env.CLAWDBOT_TASK_SCRIPT?.trim();
|
||||
const override = env.OPENCLAW_TASK_SCRIPT?.trim();
|
||||
if (override) return override;
|
||||
const scriptName = env.CLAWDBOT_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
|
||||
const scriptName = env.OPENCLAW_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
return path.join(stateDir, scriptName);
|
||||
}
|
||||
@@ -225,8 +225,8 @@ export async function installScheduledTask({
|
||||
const taskDescription =
|
||||
description ??
|
||||
formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version: environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
profile: env.OPENCLAW_PROFILE,
|
||||
version: environment?.OPENCLAW_SERVICE_VERSION ?? env.OPENCLAW_SERVICE_VERSION,
|
||||
});
|
||||
const script = buildTaskScript({
|
||||
description: taskDescription,
|
||||
|
||||
@@ -223,25 +223,25 @@ describe("buildServiceEnvironment", () => {
|
||||
} else {
|
||||
expect(env.PATH).toContain("/usr/bin");
|
||||
}
|
||||
expect(env.CLAWDBOT_GATEWAY_PORT).toBe("18789");
|
||||
expect(env.CLAWDBOT_GATEWAY_TOKEN).toBe("secret");
|
||||
expect(env.CLAWDBOT_SERVICE_MARKER).toBe("moltbot");
|
||||
expect(env.CLAWDBOT_SERVICE_KIND).toBe("gateway");
|
||||
expect(typeof env.CLAWDBOT_SERVICE_VERSION).toBe("string");
|
||||
expect(env.CLAWDBOT_SYSTEMD_UNIT).toBe("moltbot-gateway.service");
|
||||
expect(env.OPENCLAW_GATEWAY_PORT).toBe("18789");
|
||||
expect(env.OPENCLAW_GATEWAY_TOKEN).toBe("secret");
|
||||
expect(env.OPENCLAW_SERVICE_MARKER).toBe("openclaw");
|
||||
expect(env.OPENCLAW_SERVICE_KIND).toBe("gateway");
|
||||
expect(typeof env.OPENCLAW_SERVICE_VERSION).toBe("string");
|
||||
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-gateway.service");
|
||||
if (process.platform === "darwin") {
|
||||
expect(env.CLAWDBOT_LAUNCHD_LABEL).toBe("bot.molt.gateway");
|
||||
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.gateway");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses profile-specific unit and label", () => {
|
||||
const env = buildServiceEnvironment({
|
||||
env: { HOME: "/home/user", CLAWDBOT_PROFILE: "work" },
|
||||
env: { HOME: "/home/user", OPENCLAW_PROFILE: "work" },
|
||||
port: 18789,
|
||||
});
|
||||
expect(env.CLAWDBOT_SYSTEMD_UNIT).toBe("moltbot-gateway-work.service");
|
||||
expect(env.OPENCLAW_SYSTEMD_UNIT).toBe("openclaw-gateway-work.service");
|
||||
if (process.platform === "darwin") {
|
||||
expect(env.CLAWDBOT_LAUNCHD_LABEL).toBe("bot.molt.work");
|
||||
expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.work");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,24 +131,26 @@ export function buildServiceEnvironment(params: {
|
||||
launchdLabel?: string;
|
||||
}): Record<string, string | undefined> {
|
||||
const { env, port, token, launchdLabel } = params;
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
const profile = env.OPENCLAW_PROFILE;
|
||||
const resolvedLaunchdLabel =
|
||||
launchdLabel ||
|
||||
(process.platform === "darwin" ? resolveGatewayLaunchAgentLabel(profile) : undefined);
|
||||
const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`;
|
||||
const stateDir = env.OPENCLAW_STATE_DIR;
|
||||
const configPath = env.OPENCLAW_CONFIG_PATH;
|
||||
return {
|
||||
HOME: env.HOME,
|
||||
PATH: buildMinimalServicePath({ env }),
|
||||
CLAWDBOT_PROFILE: profile,
|
||||
CLAWDBOT_STATE_DIR: env.CLAWDBOT_STATE_DIR,
|
||||
CLAWDBOT_CONFIG_PATH: env.CLAWDBOT_CONFIG_PATH,
|
||||
CLAWDBOT_GATEWAY_PORT: String(port),
|
||||
CLAWDBOT_GATEWAY_TOKEN: token,
|
||||
CLAWDBOT_LAUNCHD_LABEL: resolvedLaunchdLabel,
|
||||
CLAWDBOT_SYSTEMD_UNIT: systemdUnit,
|
||||
CLAWDBOT_SERVICE_MARKER: GATEWAY_SERVICE_MARKER,
|
||||
CLAWDBOT_SERVICE_KIND: GATEWAY_SERVICE_KIND,
|
||||
CLAWDBOT_SERVICE_VERSION: VERSION,
|
||||
OPENCLAW_PROFILE: profile,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENCLAW_GATEWAY_PORT: String(port),
|
||||
OPENCLAW_GATEWAY_TOKEN: token,
|
||||
OPENCLAW_LAUNCHD_LABEL: resolvedLaunchdLabel,
|
||||
OPENCLAW_SYSTEMD_UNIT: systemdUnit,
|
||||
OPENCLAW_SERVICE_MARKER: GATEWAY_SERVICE_MARKER,
|
||||
OPENCLAW_SERVICE_KIND: GATEWAY_SERVICE_KIND,
|
||||
OPENCLAW_SERVICE_VERSION: VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,18 +158,20 @@ export function buildNodeServiceEnvironment(params: {
|
||||
env: Record<string, string | undefined>;
|
||||
}): Record<string, string | undefined> {
|
||||
const { env } = params;
|
||||
const stateDir = env.OPENCLAW_STATE_DIR;
|
||||
const configPath = env.OPENCLAW_CONFIG_PATH;
|
||||
return {
|
||||
HOME: env.HOME,
|
||||
PATH: buildMinimalServicePath({ env }),
|
||||
CLAWDBOT_STATE_DIR: env.CLAWDBOT_STATE_DIR,
|
||||
CLAWDBOT_CONFIG_PATH: env.CLAWDBOT_CONFIG_PATH,
|
||||
CLAWDBOT_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(),
|
||||
CLAWDBOT_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
|
||||
CLAWDBOT_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(),
|
||||
CLAWDBOT_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME,
|
||||
CLAWDBOT_LOG_PREFIX: "node",
|
||||
CLAWDBOT_SERVICE_MARKER: NODE_SERVICE_MARKER,
|
||||
CLAWDBOT_SERVICE_KIND: NODE_SERVICE_KIND,
|
||||
CLAWDBOT_SERVICE_VERSION: VERSION,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel(),
|
||||
OPENCLAW_SYSTEMD_UNIT: resolveNodeSystemdServiceName(),
|
||||
OPENCLAW_WINDOWS_TASK_NAME: resolveNodeWindowsTaskName(),
|
||||
OPENCLAW_TASK_SCRIPT_NAME: NODE_WINDOWS_TASK_SCRIPT_NAME,
|
||||
OPENCLAW_LOG_PREFIX: "node",
|
||||
OPENCLAW_SERVICE_MARKER: NODE_SERVICE_MARKER,
|
||||
OPENCLAW_SERVICE_KIND: NODE_SERVICE_KIND,
|
||||
OPENCLAW_SERVICE_VERSION: VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ export function renderSystemdUnavailableHints(options: { wsl?: boolean } = {}):
|
||||
}
|
||||
return [
|
||||
"systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.",
|
||||
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("moltbot gateway")}\`.`,
|
||||
`If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { parseSystemdExecStart } from "./systemd-unit.js";
|
||||
|
||||
describe("parseSystemdExecStart", () => {
|
||||
it("splits on whitespace outside quotes", () => {
|
||||
const execStart = "/usr/bin/moltbot gateway start --foo bar";
|
||||
const execStart = "/usr/bin/openclaw gateway start --foo bar";
|
||||
expect(parseSystemdExecStart(execStart)).toEqual([
|
||||
"/usr/bin/moltbot",
|
||||
"/usr/bin/openclaw",
|
||||
"gateway",
|
||||
"start",
|
||||
"--foo",
|
||||
@@ -15,9 +15,9 @@ describe("parseSystemdExecStart", () => {
|
||||
});
|
||||
|
||||
it("preserves quoted arguments", () => {
|
||||
const execStart = '/usr/bin/moltbot gateway start --name "My Bot"';
|
||||
const execStart = '/usr/bin/openclaw gateway start --name "My Bot"';
|
||||
expect(parseSystemdExecStart(execStart)).toEqual([
|
||||
"/usr/bin/moltbot",
|
||||
"/usr/bin/openclaw",
|
||||
"gateway",
|
||||
"start",
|
||||
"--name",
|
||||
@@ -26,13 +26,13 @@ describe("parseSystemdExecStart", () => {
|
||||
});
|
||||
|
||||
it("parses path arguments", () => {
|
||||
const execStart = "/usr/bin/moltbot gateway start --path /tmp/moltbot";
|
||||
const execStart = "/usr/bin/openclaw gateway start --path /tmp/openclaw";
|
||||
expect(parseSystemdExecStart(execStart)).toEqual([
|
||||
"/usr/bin/moltbot",
|
||||
"/usr/bin/openclaw",
|
||||
"gateway",
|
||||
"start",
|
||||
"--path",
|
||||
"/tmp/moltbot",
|
||||
"/tmp/openclaw",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export function buildSystemdUnit({
|
||||
environment?: Record<string, string | undefined>;
|
||||
}): string {
|
||||
const execStart = programArguments.map(systemdEscapeArg).join(" ");
|
||||
const descriptionLine = `Description=${description?.trim() || "Moltbot Gateway"}`;
|
||||
const descriptionLine = `Description=${description?.trim() || "OpenClaw Gateway"}`;
|
||||
const workingDirLine = workingDirectory
|
||||
? `WorkingDirectory=${systemdEscapeArg(workingDirectory)}`
|
||||
: null;
|
||||
|
||||
@@ -21,52 +21,52 @@ describe("systemd runtime parsing", () => {
|
||||
});
|
||||
|
||||
describe("resolveSystemdUserUnitPath", () => {
|
||||
it("uses default service name when CLAWDBOT_PROFILE is default", () => {
|
||||
const env = { HOME: "/home/test", CLAWDBOT_PROFILE: "default" };
|
||||
it("uses default service name when OPENCLAW_PROFILE is default", () => {
|
||||
const env = { HOME: "/home/test", OPENCLAW_PROFILE: "default" };
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/moltbot-gateway.service",
|
||||
"/home/test/.config/systemd/user/openclaw-gateway.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses default service name when CLAWDBOT_PROFILE is unset", () => {
|
||||
it("uses default service name when OPENCLAW_PROFILE is unset", () => {
|
||||
const env = { HOME: "/home/test" };
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/moltbot-gateway.service",
|
||||
"/home/test/.config/systemd/user/openclaw-gateway.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses profile-specific service name when CLAWDBOT_PROFILE is set to a custom value", () => {
|
||||
const env = { HOME: "/home/test", CLAWDBOT_PROFILE: "jbphoenix" };
|
||||
it("uses profile-specific service name when OPENCLAW_PROFILE is set to a custom value", () => {
|
||||
const env = { HOME: "/home/test", OPENCLAW_PROFILE: "jbphoenix" };
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/moltbot-gateway-jbphoenix.service",
|
||||
"/home/test/.config/systemd/user/openclaw-gateway-jbphoenix.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers CLAWDBOT_SYSTEMD_UNIT over CLAWDBOT_PROFILE", () => {
|
||||
it("prefers OPENCLAW_SYSTEMD_UNIT over OPENCLAW_PROFILE", () => {
|
||||
const env = {
|
||||
HOME: "/home/test",
|
||||
CLAWDBOT_PROFILE: "jbphoenix",
|
||||
CLAWDBOT_SYSTEMD_UNIT: "custom-unit",
|
||||
OPENCLAW_PROFILE: "jbphoenix",
|
||||
OPENCLAW_SYSTEMD_UNIT: "custom-unit",
|
||||
};
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/custom-unit.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles CLAWDBOT_SYSTEMD_UNIT with .service suffix", () => {
|
||||
it("handles OPENCLAW_SYSTEMD_UNIT with .service suffix", () => {
|
||||
const env = {
|
||||
HOME: "/home/test",
|
||||
CLAWDBOT_SYSTEMD_UNIT: "custom-unit.service",
|
||||
OPENCLAW_SYSTEMD_UNIT: "custom-unit.service",
|
||||
};
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/custom-unit.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from CLAWDBOT_SYSTEMD_UNIT", () => {
|
||||
it("trims whitespace from OPENCLAW_SYSTEMD_UNIT", () => {
|
||||
const env = {
|
||||
HOME: "/home/test",
|
||||
CLAWDBOT_SYSTEMD_UNIT: " custom-unit ",
|
||||
OPENCLAW_SYSTEMD_UNIT: " custom-unit ",
|
||||
};
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/custom-unit.service",
|
||||
@@ -74,23 +74,23 @@ describe("resolveSystemdUserUnitPath", () => {
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'Default' profile", () => {
|
||||
const env = { HOME: "/home/test", CLAWDBOT_PROFILE: "Default" };
|
||||
const env = { HOME: "/home/test", OPENCLAW_PROFILE: "Default" };
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/moltbot-gateway.service",
|
||||
"/home/test/.config/systemd/user/openclaw-gateway.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles case-insensitive 'DEFAULT' profile", () => {
|
||||
const env = { HOME: "/home/test", CLAWDBOT_PROFILE: "DEFAULT" };
|
||||
const env = { HOME: "/home/test", OPENCLAW_PROFILE: "DEFAULT" };
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/moltbot-gateway.service",
|
||||
"/home/test/.config/systemd/user/openclaw-gateway.service",
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from CLAWDBOT_PROFILE", () => {
|
||||
const env = { HOME: "/home/test", CLAWDBOT_PROFILE: " myprofile " };
|
||||
it("trims whitespace from OPENCLAW_PROFILE", () => {
|
||||
const env = { HOME: "/home/test", OPENCLAW_PROFILE: " myprofile " };
|
||||
expect(resolveSystemdUserUnitPath(env)).toBe(
|
||||
"/home/test/.config/systemd/user/moltbot-gateway-myprofile.service",
|
||||
"/home/test/.config/systemd/user/openclaw-gateway-myprofile.service",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,11 +39,11 @@ function resolveSystemdUnitPathForName(
|
||||
}
|
||||
|
||||
function resolveSystemdServiceName(env: Record<string, string | undefined>): string {
|
||||
const override = env.CLAWDBOT_SYSTEMD_UNIT?.trim();
|
||||
const override = env.OPENCLAW_SYSTEMD_UNIT?.trim();
|
||||
if (override) {
|
||||
return override.endsWith(".service") ? override.slice(0, -".service".length) : override;
|
||||
}
|
||||
return resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
return resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
|
||||
}
|
||||
|
||||
function resolveSystemdUnitPath(env: Record<string, string | undefined>): string {
|
||||
@@ -202,8 +202,8 @@ export async function installSystemdService({
|
||||
const serviceDescription =
|
||||
description ??
|
||||
formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version: environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
profile: env.OPENCLAW_PROFILE,
|
||||
version: environment?.OPENCLAW_SERVICE_VERSION ?? env.OPENCLAW_SERVICE_VERSION,
|
||||
});
|
||||
const unit = buildSystemdUnit({
|
||||
description: serviceDescription,
|
||||
@@ -213,7 +213,7 @@ export async function installSystemdService({
|
||||
});
|
||||
await fs.writeFile(unitPath, unit, "utf8");
|
||||
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const reload = await execSystemctl(["--user", "daemon-reload"]);
|
||||
if (reload.code !== 0) {
|
||||
@@ -244,7 +244,7 @@ export async function uninstallSystemdService({
|
||||
stdout: NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
|
||||
const unitName = `${serviceName}.service`;
|
||||
await execSystemctl(["--user", "disable", "--now", unitName]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user