feat: improve gateway services and auto-reply commands
This commit is contained in:
168
src/daemon/constants.test.ts
Normal file
168
src/daemon/constants.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
|
||||
describe("resolveGatewayLaunchAgentLabel", () => {
|
||||
it("returns default label when no profile is set", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel();
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
expect(result).toBe("com.clawdbot.gateway");
|
||||
});
|
||||
|
||||
it("returns default label when profile is undefined", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(undefined);
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns default label when profile is 'default'", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("default");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns default label when profile is 'Default' (case-insensitive)", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("Default");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns profile-specific label when profile is set", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("dev");
|
||||
expect(result).toBe("com.clawdbot.dev");
|
||||
});
|
||||
|
||||
it("returns profile-specific label for custom profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("work");
|
||||
expect(result).toBe("com.clawdbot.work");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(" staging ");
|
||||
expect(result).toBe("com.clawdbot.staging");
|
||||
});
|
||||
|
||||
it("returns default label for empty string profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel("");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
|
||||
it("returns default label for whitespace-only profile", () => {
|
||||
const result = resolveGatewayLaunchAgentLabel(" ");
|
||||
expect(result).toBe(GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
});
|
||||
});
|
||||
|
||||
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("clawdbot-gateway");
|
||||
});
|
||||
|
||||
it("returns default service name when profile is undefined", () => {
|
||||
const result = resolveGatewaySystemdServiceName(undefined);
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
|
||||
it("returns default service name when profile is 'default'", () => {
|
||||
const result = resolveGatewaySystemdServiceName("default");
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
|
||||
it("returns default service name when profile is 'DEFAULT' (case-insensitive)", () => {
|
||||
const result = resolveGatewaySystemdServiceName("DEFAULT");
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
|
||||
it("returns profile-specific service name when profile is set", () => {
|
||||
const result = resolveGatewaySystemdServiceName("dev");
|
||||
expect(result).toBe("clawdbot-gateway-dev");
|
||||
});
|
||||
|
||||
it("returns profile-specific service name for custom profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName("production");
|
||||
expect(result).toBe("clawdbot-gateway-production");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName(" test ");
|
||||
expect(result).toBe("clawdbot-gateway-test");
|
||||
});
|
||||
|
||||
it("returns default service name for empty string profile", () => {
|
||||
const result = resolveGatewaySystemdServiceName("");
|
||||
expect(result).toBe(GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
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("Clawdbot Gateway");
|
||||
});
|
||||
|
||||
it("returns default task name when profile is undefined", () => {
|
||||
const result = resolveGatewayWindowsTaskName(undefined);
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
|
||||
it("returns default task name when profile is 'default'", () => {
|
||||
const result = resolveGatewayWindowsTaskName("default");
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
|
||||
it("returns default task name when profile is 'DeFaUlT' (case-insensitive)", () => {
|
||||
const result = resolveGatewayWindowsTaskName("DeFaUlT");
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
|
||||
it("returns profile-specific task name when profile is set", () => {
|
||||
const result = resolveGatewayWindowsTaskName("dev");
|
||||
expect(result).toBe("Clawdbot Gateway (dev)");
|
||||
});
|
||||
|
||||
it("returns profile-specific task name for custom profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName("work");
|
||||
expect(result).toBe("Clawdbot Gateway (work)");
|
||||
});
|
||||
|
||||
it("trims whitespace from profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName(" ci ");
|
||||
expect(result).toBe("Clawdbot Gateway (ci)");
|
||||
});
|
||||
|
||||
it("returns default task name for empty string profile", () => {
|
||||
const result = resolveGatewayWindowsTaskName("");
|
||||
expect(result).toBe(GATEWAY_WINDOWS_TASK_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatGatewayServiceDescription", () => {
|
||||
it("returns default description when no profile/version", () => {
|
||||
expect(formatGatewayServiceDescription()).toBe("Clawdbot Gateway");
|
||||
});
|
||||
|
||||
it("includes profile when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ profile: "work" }),
|
||||
).toBe("Clawdbot Gateway (profile: work)");
|
||||
});
|
||||
|
||||
it("includes version when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ version: "2026.1.10" }),
|
||||
).toBe("Clawdbot Gateway (v2026.1.10)");
|
||||
});
|
||||
|
||||
it("includes profile and version when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ profile: "dev", version: "1.2.3" }),
|
||||
).toBe("Clawdbot Gateway (profile: dev, v1.2.3)");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
export const GATEWAY_LAUNCH_AGENT_LABEL = "com.clawdbot.gateway";
|
||||
export const GATEWAY_SYSTEMD_SERVICE_NAME = "clawdbot-gateway";
|
||||
export const GATEWAY_WINDOWS_TASK_NAME = "Clawdbot Gateway";
|
||||
export const GATEWAY_SERVICE_MARKER = "clawdbot";
|
||||
export const GATEWAY_SERVICE_KIND = "gateway";
|
||||
export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = [
|
||||
"com.steipete.clawdbot.gateway",
|
||||
"com.steipete.clawdis.gateway",
|
||||
@@ -8,3 +10,46 @@ export const LEGACY_GATEWAY_LAUNCH_AGENT_LABELS = [
|
||||
];
|
||||
export const LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES = ["clawdis-gateway"];
|
||||
export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES = ["Clawdis Gateway"];
|
||||
|
||||
export function resolveGatewayLaunchAgentLabel(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
}
|
||||
return `com.clawdbot.${trimmed}`;
|
||||
}
|
||||
|
||||
function normalizeGatewayProfile(profile?: string): string | null {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveGatewaySystemdServiceName(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_SYSTEMD_SERVICE_NAME;
|
||||
}
|
||||
return `clawdbot-gateway-${trimmed}`;
|
||||
}
|
||||
|
||||
export function resolveGatewayWindowsTaskName(profile?: string): string {
|
||||
const trimmed = profile?.trim();
|
||||
if (!trimmed || trimmed.toLowerCase() === "default") {
|
||||
return GATEWAY_WINDOWS_TASK_NAME;
|
||||
}
|
||||
return `Clawdbot Gateway (${trimmed})`;
|
||||
}
|
||||
|
||||
export function formatGatewayServiceDescription(params?: {
|
||||
profile?: string;
|
||||
version?: string;
|
||||
}): string {
|
||||
const profile = normalizeGatewayProfile(params?.profile);
|
||||
const version = params?.version?.trim();
|
||||
const parts: string[] = [];
|
||||
if (profile) parts.push(`profile: ${profile}`);
|
||||
if (version) parts.push(`v${version}`);
|
||||
if (parts.length === 0) return "Clawdbot Gateway";
|
||||
return `Clawdbot Gateway (${parts.join(", ")})`;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS,
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||
LEGACY_GATEWAY_WINDOWS_TASK_NAMES,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
|
||||
export type ExtraGatewayService = {
|
||||
@@ -26,20 +28,32 @@ export type FindExtraGatewayServicesOptions = {
|
||||
const EXTRA_MARKERS = ["clawdbot", "clawdis"];
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export function renderGatewayServiceCleanupHints(): string[] {
|
||||
export function renderGatewayServiceCleanupHints(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
): string[] {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin":
|
||||
case "darwin": {
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
return [
|
||||
`launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
`rm ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
|
||||
`launchctl bootout gui/$UID/${label}`,
|
||||
`rm ~/Library/LaunchAgents/${label}.plist`,
|
||||
];
|
||||
case "linux":
|
||||
}
|
||||
case "linux": {
|
||||
const unit = resolveGatewaySystemdServiceName(profile);
|
||||
return [
|
||||
`systemctl --user disable --now ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
`rm ~/.config/systemd/user/${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
|
||||
`systemctl --user disable --now ${unit}.service`,
|
||||
`rm ~/.config/systemd/user/${unit}.service`,
|
||||
];
|
||||
case "win32":
|
||||
return [`schtasks /Delete /TN "${GATEWAY_WINDOWS_TASK_NAME}" /F`];
|
||||
}
|
||||
case "win32": {
|
||||
const task = resolveGatewayWindowsTaskName(profile);
|
||||
return [`schtasks /Delete /TN "${task}" /F`];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -56,6 +70,42 @@ function containsMarker(content: string): boolean {
|
||||
return EXTRA_MARKERS.some((marker) => lower.includes(marker));
|
||||
}
|
||||
|
||||
function hasGatewayServiceMarker(content: string): boolean {
|
||||
const lower = content.toLowerCase();
|
||||
return (
|
||||
lower.includes("clawdbot_service_marker") &&
|
||||
lower.includes(GATEWAY_SERVICE_MARKER.toLowerCase()) &&
|
||||
lower.includes("clawdbot_service_kind") &&
|
||||
lower.includes(GATEWAY_SERVICE_KIND.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
function isClawdbotGatewayLaunchdService(
|
||||
label: string,
|
||||
contents: string,
|
||||
): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
const lowerContents = contents.toLowerCase();
|
||||
if (!lowerContents.includes("gateway")) return false;
|
||||
return label.startsWith("com.clawdbot.");
|
||||
}
|
||||
|
||||
function isClawdbotGatewaySystemdService(
|
||||
name: string,
|
||||
contents: string,
|
||||
): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
if (!name.startsWith("clawdbot-gateway")) return false;
|
||||
return contents.toLowerCase().includes("gateway");
|
||||
}
|
||||
|
||||
function isClawdbotGatewayTaskName(name: string): boolean {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
const defaultName = resolveGatewayWindowsTaskName().toLowerCase();
|
||||
return normalized === defaultName || normalized.startsWith("clawdbot gateway");
|
||||
}
|
||||
|
||||
function tryExtractPlistLabel(contents: string): string | null {
|
||||
const match = contents.match(
|
||||
/<key>Label<\/key>\s*<string>([\s\S]*?)<\/string>/i,
|
||||
@@ -66,14 +116,14 @@ function tryExtractPlistLabel(contents: string): string | null {
|
||||
|
||||
function isIgnoredLaunchdLabel(label: string): boolean {
|
||||
return (
|
||||
label === GATEWAY_LAUNCH_AGENT_LABEL ||
|
||||
label === resolveGatewayLaunchAgentLabel() ||
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS.includes(label)
|
||||
);
|
||||
}
|
||||
|
||||
function isIgnoredSystemdName(name: string): boolean {
|
||||
return (
|
||||
name === GATEWAY_SYSTEMD_SERVICE_NAME ||
|
||||
name === resolveGatewaySystemdServiceName() ||
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES.includes(name)
|
||||
);
|
||||
}
|
||||
@@ -104,6 +154,7 @@ async function scanLaunchdDir(params: {
|
||||
if (!containsMarker(contents)) continue;
|
||||
const label = tryExtractPlistLabel(contents) ?? labelFromName;
|
||||
if (isIgnoredLaunchdLabel(label)) continue;
|
||||
if (isClawdbotGatewayLaunchdService(label, contents)) continue;
|
||||
results.push({
|
||||
platform: "darwin",
|
||||
label,
|
||||
@@ -139,6 +190,7 @@ async function scanSystemdDir(params: {
|
||||
continue;
|
||||
}
|
||||
if (!containsMarker(contents)) continue;
|
||||
if (isClawdbotGatewaySystemdService(name, contents)) continue;
|
||||
results.push({
|
||||
platform: "linux",
|
||||
label: entry,
|
||||
@@ -302,7 +354,7 @@ export async function findExtraGatewayServices(
|
||||
for (const task of tasks) {
|
||||
const name = task.name.trim();
|
||||
if (!name) continue;
|
||||
if (name === GATEWAY_WINDOWS_TASK_NAME) continue;
|
||||
if (isClawdbotGatewayTaskName(name)) continue;
|
||||
if (LEGACY_GATEWAY_WINDOWS_TASK_NAMES.includes(name)) continue;
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
|
||||
|
||||
@@ -7,6 +7,8 @@ import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -34,7 +36,10 @@ function resolveLaunchAgentPlistPathForLabel(
|
||||
export function resolveLaunchAgentPlistPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
return resolveLaunchAgentPlistPathForLabel(env, GATEWAY_LAUNCH_AGENT_LABEL);
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
return resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
}
|
||||
|
||||
export function resolveGatewayLogPaths(
|
||||
@@ -162,6 +167,7 @@ export async function readLaunchAgentProgramArguments(
|
||||
|
||||
export function buildLaunchAgentPlist({
|
||||
label = GATEWAY_LAUNCH_AGENT_LABEL,
|
||||
comment,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
stdoutPath,
|
||||
@@ -169,6 +175,7 @@ export function buildLaunchAgentPlist({
|
||||
environment,
|
||||
}: {
|
||||
label?: string;
|
||||
comment?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
stdoutPath: string;
|
||||
@@ -183,6 +190,12 @@ export function buildLaunchAgentPlist({
|
||||
<key>WorkingDirectory</key>
|
||||
<string>${plistEscape(workingDirectory)}</string>`
|
||||
: "";
|
||||
const commentXml =
|
||||
comment && comment.trim()
|
||||
? `
|
||||
<key>Comment</key>
|
||||
<string>${plistEscape(comment.trim())}</string>`
|
||||
: "";
|
||||
const envXml = renderEnvDict(environment);
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
@@ -190,6 +203,7 @@ export function buildLaunchAgentPlist({
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>${plistEscape(label)}</string>
|
||||
${commentXml}
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
@@ -271,9 +285,9 @@ export function parseLaunchctlPrint(output: string): LaunchctlPrintInfo {
|
||||
return info;
|
||||
}
|
||||
|
||||
export async function isLaunchAgentLoaded(): Promise<boolean> {
|
||||
export async function isLaunchAgentLoaded(profile?: string): Promise<boolean> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
const res = await execLaunchctl(["print", `${domain}/${label}`]);
|
||||
return res.code === 0;
|
||||
}
|
||||
@@ -294,7 +308,9 @@ export async function readLaunchAgentRuntime(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
const res = await execLaunchctl(["print", `${domain}/${label}`]);
|
||||
if (res.code !== 0) {
|
||||
return {
|
||||
@@ -418,7 +434,10 @@ export async function uninstallLaunchAgent({
|
||||
|
||||
const home = resolveHomeDir(env);
|
||||
const trashDir = path.join(home, ".Trash");
|
||||
const dest = path.join(trashDir, `${GATEWAY_LAUNCH_AGENT_LABEL}.plist`);
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
const dest = path.join(trashDir, `${label}.plist`);
|
||||
try {
|
||||
await fs.mkdir(trashDir, { recursive: true });
|
||||
await fs.rename(plistPath, dest);
|
||||
@@ -443,11 +462,13 @@ function isLaunchctlNotLoaded(res: {
|
||||
|
||||
export async function stopLaunchAgent({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
const res = await execLaunchctl(["bootout", `${domain}/${label}`]);
|
||||
if (res.code !== 0 && !isLaunchctlNotLoaded(res)) {
|
||||
throw new Error(
|
||||
@@ -474,6 +495,9 @@ export async function installLaunchAgent({
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
|
||||
const domain = resolveGuiDomain();
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
for (const legacyLabel of LEGACY_GATEWAY_LAUNCH_AGENT_LABELS) {
|
||||
const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(
|
||||
env,
|
||||
@@ -488,10 +512,17 @@ export async function installLaunchAgent({
|
||||
}
|
||||
}
|
||||
|
||||
const plistPath = resolveLaunchAgentPlistPath(env);
|
||||
const plistPath = resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
await fs.mkdir(path.dirname(plistPath), { recursive: true });
|
||||
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const plist = buildLaunchAgentPlist({
|
||||
label,
|
||||
comment: description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
stdoutPath,
|
||||
@@ -508,12 +539,8 @@ export async function installLaunchAgent({
|
||||
`launchctl bootstrap failed: ${boot.stderr || boot.stdout}`.trim(),
|
||||
);
|
||||
}
|
||||
await execLaunchctl(["enable", `${domain}/${GATEWAY_LAUNCH_AGENT_LABEL}`]);
|
||||
await execLaunchctl([
|
||||
"kickstart",
|
||||
"-k",
|
||||
`${domain}/${GATEWAY_LAUNCH_AGENT_LABEL}`,
|
||||
]);
|
||||
await execLaunchctl(["enable", `${domain}/${label}`]);
|
||||
await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
|
||||
stdout.write(`${formatLine("Installed LaunchAgent", plistPath)}\n`);
|
||||
stdout.write(`${formatLine("Logs", stdoutPath)}\n`);
|
||||
@@ -522,11 +549,13 @@ export async function installLaunchAgent({
|
||||
|
||||
export async function restartLaunchAgent({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label = GATEWAY_LAUNCH_AGENT_LABEL;
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
const res = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
|
||||
@@ -5,8 +5,9 @@ import { promisify } from "node:util";
|
||||
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
LEGACY_GATEWAY_WINDOWS_TASK_NAMES,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewayWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -28,7 +29,10 @@ function resolveTaskScriptPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
const home = resolveHomeDir(env);
|
||||
return path.join(home, ".clawdbot", "gateway.cmd");
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
||||
const suffix =
|
||||
profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
return path.join(home, `.clawdbot${suffix}`, "gateway.cmd");
|
||||
}
|
||||
|
||||
function resolveLegacyTaskScriptPath(
|
||||
@@ -78,18 +82,32 @@ function parseCommandLine(value: string): string[] {
|
||||
|
||||
export async function readScheduledTaskCommand(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<{ programArguments: string[]; workingDirectory?: string } | null> {
|
||||
): Promise<{
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string>;
|
||||
} | null> {
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
try {
|
||||
const content = await fs.readFile(scriptPath, "utf8");
|
||||
let workingDirectory = "";
|
||||
let commandLine = "";
|
||||
const environment: Record<string, string> = {};
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) continue;
|
||||
if (line.startsWith("@echo")) continue;
|
||||
if (line.toLowerCase().startsWith("rem ")) continue;
|
||||
if (line.toLowerCase().startsWith("set ")) continue;
|
||||
if (line.toLowerCase().startsWith("set ")) {
|
||||
const assignment = line.slice(4).trim();
|
||||
const index = assignment.indexOf("=");
|
||||
if (index > 0) {
|
||||
const key = assignment.slice(0, index).trim();
|
||||
const value = assignment.slice(index + 1).trim();
|
||||
if (key) environment[key] = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.toLowerCase().startsWith("cd /d ")) {
|
||||
workingDirectory = line
|
||||
.slice("cd /d ".length)
|
||||
@@ -104,6 +122,7 @@ export async function readScheduledTaskCommand(
|
||||
return {
|
||||
programArguments: parseCommandLine(commandLine),
|
||||
...(workingDirectory ? { workingDirectory } : {}),
|
||||
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -129,15 +148,20 @@ export function parseSchtasksQuery(output: string): ScheduledTaskInfo {
|
||||
}
|
||||
|
||||
function buildTaskScript({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
}: {
|
||||
description?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string | undefined>;
|
||||
}): string {
|
||||
const lines: string[] = ["@echo off"];
|
||||
if (description?.trim()) {
|
||||
lines.push(`rem ${description.trim()}`);
|
||||
}
|
||||
if (workingDirectory) {
|
||||
lines.push(`cd /d ${quoteCmdArg(workingDirectory)}`);
|
||||
}
|
||||
@@ -208,13 +232,20 @@ export async function installScheduledTask({
|
||||
await assertSchtasksAvailable();
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const script = buildTaskScript({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
});
|
||||
await fs.writeFile(scriptPath, script, "utf8");
|
||||
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
const quotedScript = quoteCmdArg(scriptPath);
|
||||
const create = await execSchtasks([
|
||||
"/Create",
|
||||
@@ -224,7 +255,7 @@ export async function installScheduledTask({
|
||||
"/RL",
|
||||
"LIMITED",
|
||||
"/TN",
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
taskName,
|
||||
"/TR",
|
||||
quotedScript,
|
||||
]);
|
||||
@@ -234,10 +265,8 @@ export async function installScheduledTask({
|
||||
);
|
||||
}
|
||||
|
||||
await execSchtasks(["/Run", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
stdout.write(
|
||||
`${formatLine("Installed Scheduled Task", GATEWAY_WINDOWS_TASK_NAME)}\n`,
|
||||
);
|
||||
await execSchtasks(["/Run", "/TN", taskName]);
|
||||
stdout.write(`${formatLine("Installed Scheduled Task", taskName)}\n`);
|
||||
stdout.write(`${formatLine("Task script", scriptPath)}\n`);
|
||||
return { scriptPath };
|
||||
}
|
||||
@@ -250,7 +279,8 @@ export async function uninstallScheduledTask({
|
||||
stdout: NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
await execSchtasks(["/Delete", "/F", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
await execSchtasks(["/Delete", "/F", "/TN", taskName]);
|
||||
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
try {
|
||||
@@ -272,42 +302,52 @@ function isTaskNotRunning(res: {
|
||||
|
||||
export async function stopScheduledTask({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
const res = await execSchtasks(["/End", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
const res = await execSchtasks(["/End", "/TN", taskName]);
|
||||
if (res.code !== 0 && !isTaskNotRunning(res)) {
|
||||
throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(
|
||||
`${formatLine("Stopped Scheduled Task", GATEWAY_WINDOWS_TASK_NAME)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Stopped Scheduled Task", taskName)}\n`);
|
||||
}
|
||||
|
||||
export async function restartScheduledTask({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSchtasksAvailable();
|
||||
await execSchtasks(["/End", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const res = await execSchtasks(["/Run", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
await execSchtasks(["/End", "/TN", taskName]);
|
||||
const res = await execSchtasks(["/Run", "/TN", taskName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(`schtasks run failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(
|
||||
`${formatLine("Restarted Scheduled Task", GATEWAY_WINDOWS_TASK_NAME)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Restarted Scheduled Task", taskName)}\n`);
|
||||
}
|
||||
|
||||
export async function isScheduledTaskInstalled(): Promise<boolean> {
|
||||
export async function isScheduledTaskInstalled(
|
||||
profile?: string,
|
||||
): Promise<boolean> {
|
||||
await assertSchtasksAvailable();
|
||||
const res = await execSchtasks(["/Query", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName]);
|
||||
return res.code === 0;
|
||||
}
|
||||
|
||||
export async function readScheduledTaskRuntime(): Promise<GatewayServiceRuntime> {
|
||||
export async function readScheduledTaskRuntime(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
try {
|
||||
await assertSchtasksAvailable();
|
||||
} catch (err) {
|
||||
@@ -316,10 +356,11 @@ export async function readScheduledTaskRuntime(): Promise<GatewayServiceRuntime>
|
||||
detail: String(err),
|
||||
};
|
||||
}
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
const res = await execSchtasks([
|
||||
"/Query",
|
||||
"/TN",
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
taskName,
|
||||
"/V",
|
||||
"/FO",
|
||||
"LIST",
|
||||
|
||||
@@ -58,5 +58,23 @@ describe("buildServiceEnvironment", () => {
|
||||
}
|
||||
expect(env.CLAWDBOT_GATEWAY_PORT).toBe("18789");
|
||||
expect(env.CLAWDBOT_GATEWAY_TOKEN).toBe("secret");
|
||||
expect(env.CLAWDBOT_SERVICE_MARKER).toBe("clawdbot");
|
||||
expect(env.CLAWDBOT_SERVICE_KIND).toBe("gateway");
|
||||
expect(typeof env.CLAWDBOT_SERVICE_VERSION).toBe("string");
|
||||
expect(env.CLAWDBOT_SYSTEMD_UNIT).toBe("clawdbot-gateway.service");
|
||||
if (process.platform === "darwin") {
|
||||
expect(env.CLAWDBOT_LAUNCHD_LABEL).toBe("com.clawdbot.gateway");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses profile-specific unit and label", () => {
|
||||
const env = buildServiceEnvironment({
|
||||
env: { HOME: "/home/user", CLAWDBOT_PROFILE: "work" },
|
||||
port: 18789,
|
||||
});
|
||||
expect(env.CLAWDBOT_SYSTEMD_UNIT).toBe("clawdbot-gateway-work.service");
|
||||
if (process.platform === "darwin") {
|
||||
expect(env.CLAWDBOT_LAUNCHD_LABEL).toBe("com.clawdbot.work");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { VERSION } from "../version.js";
|
||||
import {
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "./constants.js";
|
||||
|
||||
export type MinimalServicePathOptions = {
|
||||
platform?: NodeJS.Platform;
|
||||
extraDirs?: string[];
|
||||
@@ -59,13 +67,24 @@ export function buildServiceEnvironment(params: {
|
||||
launchdLabel?: string;
|
||||
}): Record<string, string | undefined> {
|
||||
const { env, port, token, launchdLabel } = params;
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
const resolvedLaunchdLabel =
|
||||
launchdLabel ||
|
||||
(process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(profile)
|
||||
: undefined);
|
||||
const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`;
|
||||
return {
|
||||
PATH: buildMinimalServicePath({ env }),
|
||||
CLAWDBOT_PROFILE: env.CLAWDBOT_PROFILE,
|
||||
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: launchdLabel,
|
||||
CLAWDBOT_LAUNCHD_LABEL: resolvedLaunchdLabel,
|
||||
CLAWDBOT_SYSTEMD_UNIT: systemdUnit,
|
||||
CLAWDBOT_SERVICE_MARKER: GATEWAY_SERVICE_MARKER,
|
||||
CLAWDBOT_SERVICE_KIND: GATEWAY_SERVICE_KIND,
|
||||
CLAWDBOT_SERVICE_VERSION: VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,11 +44,15 @@ export type GatewayService = {
|
||||
env: Record<string, string | undefined>;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}) => Promise<void>;
|
||||
stop: (args: { stdout: NodeJS.WritableStream }) => Promise<void>;
|
||||
restart: (args: { stdout: NodeJS.WritableStream }) => Promise<void>;
|
||||
isLoaded: (args: {
|
||||
env: Record<string, string | undefined>;
|
||||
}) => Promise<boolean>;
|
||||
stop: (args: {
|
||||
profile?: string;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}) => Promise<void>;
|
||||
restart: (args: {
|
||||
profile?: string;
|
||||
stdout: NodeJS.WritableStream;
|
||||
}) => Promise<void>;
|
||||
isLoaded: (args: { profile?: string }) => Promise<boolean>;
|
||||
readCommand: (env: Record<string, string | undefined>) => Promise<{
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
@@ -73,12 +77,15 @@ export function resolveGatewayService(): GatewayService {
|
||||
await uninstallLaunchAgent(args);
|
||||
},
|
||||
stop: async (args) => {
|
||||
await stopLaunchAgent(args);
|
||||
await stopLaunchAgent({ stdout: args.stdout, profile: args.profile });
|
||||
},
|
||||
restart: async (args) => {
|
||||
await restartLaunchAgent(args);
|
||||
await restartLaunchAgent({
|
||||
stdout: args.stdout,
|
||||
profile: args.profile,
|
||||
});
|
||||
},
|
||||
isLoaded: async () => isLaunchAgentLoaded(),
|
||||
isLoaded: async (args) => isLaunchAgentLoaded(args.profile),
|
||||
readCommand: readLaunchAgentProgramArguments,
|
||||
readRuntime: readLaunchAgentRuntime,
|
||||
};
|
||||
@@ -96,14 +103,17 @@ export function resolveGatewayService(): GatewayService {
|
||||
await uninstallSystemdService(args);
|
||||
},
|
||||
stop: async (args) => {
|
||||
await stopSystemdService(args);
|
||||
await stopSystemdService({ stdout: args.stdout, profile: args.profile });
|
||||
},
|
||||
restart: async (args) => {
|
||||
await restartSystemdService(args);
|
||||
await restartSystemdService({
|
||||
stdout: args.stdout,
|
||||
profile: args.profile,
|
||||
});
|
||||
},
|
||||
isLoaded: async () => isSystemdServiceEnabled(),
|
||||
isLoaded: async (args) => isSystemdServiceEnabled(args.profile),
|
||||
readCommand: readSystemdServiceExecStart,
|
||||
readRuntime: async () => await readSystemdServiceRuntime(),
|
||||
readRuntime: async (env) => await readSystemdServiceRuntime(env),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,14 +129,17 @@ export function resolveGatewayService(): GatewayService {
|
||||
await uninstallScheduledTask(args);
|
||||
},
|
||||
stop: async (args) => {
|
||||
await stopScheduledTask(args);
|
||||
await stopScheduledTask({ stdout: args.stdout, profile: args.profile });
|
||||
},
|
||||
restart: async (args) => {
|
||||
await restartScheduledTask(args);
|
||||
await restartScheduledTask({
|
||||
stdout: args.stdout,
|
||||
profile: args.profile,
|
||||
});
|
||||
},
|
||||
isLoaded: async () => isScheduledTaskInstalled(),
|
||||
isLoaded: async (args) => isScheduledTaskInstalled(args.profile),
|
||||
readCommand: readScheduledTaskCommand,
|
||||
readRuntime: async () => await readScheduledTaskRuntime(),
|
||||
readRuntime: async (env) => await readScheduledTaskRuntime(env),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import { promisify } from "node:util";
|
||||
import { runCommandWithTimeout, runExec } from "../process/exec.js";
|
||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
LEGACY_GATEWAY_SYSTEMD_SERVICE_NAMES,
|
||||
formatGatewayServiceDescription,
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "./constants.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
@@ -33,10 +34,22 @@ function resolveSystemdUnitPathForName(
|
||||
return path.join(home, ".config", "systemd", "user", `${name}.service`);
|
||||
}
|
||||
|
||||
function resolveSystemdServiceName(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
const override = env.CLAWDBOT_SYSTEMD_UNIT?.trim();
|
||||
if (override) {
|
||||
return override.endsWith(".service")
|
||||
? override.slice(0, -".service".length)
|
||||
: override;
|
||||
}
|
||||
return resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
}
|
||||
|
||||
function resolveSystemdUnitPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
return resolveSystemdUnitPathForName(env, GATEWAY_SYSTEMD_SERVICE_NAME);
|
||||
return resolveSystemdUnitPathForName(env, resolveSystemdServiceName(env));
|
||||
}
|
||||
|
||||
export function resolveSystemdUserUnitPath(
|
||||
@@ -137,22 +150,25 @@ function renderEnvLines(
|
||||
}
|
||||
|
||||
function buildSystemdUnit({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
}: {
|
||||
description?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string | undefined>;
|
||||
}): string {
|
||||
const execStart = programArguments.map(systemdEscapeArg).join(" ");
|
||||
const descriptionLine = `Description=${description?.trim() || "Clawdbot Gateway"}`;
|
||||
const workingDirLine = workingDirectory
|
||||
? `WorkingDirectory=${systemdEscapeArg(workingDirectory)}`
|
||||
: null;
|
||||
const envLines = renderEnvLines(environment);
|
||||
return [
|
||||
"[Unit]",
|
||||
"Description=Clawdbot Gateway",
|
||||
descriptionLine,
|
||||
"After=network-online.target",
|
||||
"Wants=network-online.target",
|
||||
"",
|
||||
@@ -387,14 +403,21 @@ export async function installSystemdService({
|
||||
|
||||
const unitPath = resolveSystemdUnitPath(env);
|
||||
await fs.mkdir(path.dirname(unitPath), { recursive: true });
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const unit = buildSystemdUnit({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
});
|
||||
await fs.writeFile(unitPath, unit, "utf8");
|
||||
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const reload = await execSystemctl(["--user", "daemon-reload"]);
|
||||
if (reload.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -428,7 +451,8 @@ export async function uninstallSystemdService({
|
||||
stdout: NodeJS.WritableStream;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
const unitName = `${serviceName}.service`;
|
||||
await execSystemctl(["--user", "disable", "--now", unitName]);
|
||||
|
||||
const unitPath = resolveSystemdUnitPath(env);
|
||||
@@ -442,11 +466,14 @@ export async function uninstallSystemdService({
|
||||
|
||||
export async function stopSystemdService({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(profile);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "stop", unitName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -458,11 +485,14 @@ export async function stopSystemdService({
|
||||
|
||||
export async function restartSystemdService({
|
||||
stdout,
|
||||
profile,
|
||||
}: {
|
||||
stdout: NodeJS.WritableStream;
|
||||
profile?: string;
|
||||
}): Promise<void> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(profile);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "restart", unitName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -472,14 +502,22 @@ export async function restartSystemdService({
|
||||
stdout.write(`${formatLine("Restarted systemd service", unitName)}\n`);
|
||||
}
|
||||
|
||||
export async function isSystemdServiceEnabled(): Promise<boolean> {
|
||||
export async function isSystemdServiceEnabled(
|
||||
profile?: string,
|
||||
): Promise<boolean> {
|
||||
await assertSystemdAvailable();
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveGatewaySystemdServiceName(profile);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "is-enabled", unitName]);
|
||||
return res.code === 0;
|
||||
}
|
||||
|
||||
export async function readSystemdServiceRuntime(): Promise<GatewayServiceRuntime> {
|
||||
export async function readSystemdServiceRuntime(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
try {
|
||||
await assertSystemdAvailable();
|
||||
} catch (err) {
|
||||
@@ -488,7 +526,8 @@ export async function readSystemdServiceRuntime(): Promise<GatewayServiceRuntime
|
||||
detail: String(err),
|
||||
};
|
||||
}
|
||||
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
|
||||
const serviceName = resolveSystemdServiceName(env);
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl([
|
||||
"--user",
|
||||
"show",
|
||||
|
||||
Reference in New Issue
Block a user