chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -161,8 +161,8 @@ describe("formatGatewayServiceDescription", () => {
|
||||
});
|
||||
|
||||
it("includes profile and version when set", () => {
|
||||
expect(
|
||||
formatGatewayServiceDescription({ profile: "dev", version: "1.2.3" }),
|
||||
).toBe("Clawdbot Gateway (profile: dev, v1.2.3)");
|
||||
expect(formatGatewayServiceDescription({ profile: "dev", version: "1.2.3" })).toBe(
|
||||
"Clawdbot Gateway (profile: dev, v1.2.3)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,14 +23,12 @@ async function readLastLogLine(filePath: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLastGatewayErrorLine(
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<string | null> {
|
||||
export async function readLastGatewayErrorLine(env: NodeJS.ProcessEnv): Promise<string | null> {
|
||||
const { stdoutPath, stderrPath } = resolveGatewayLogPaths(env);
|
||||
const stderrRaw = await fs.readFile(stderrPath, "utf8").catch(() => "");
|
||||
const stdoutRaw = await fs.readFile(stdoutPath, "utf8").catch(() => "");
|
||||
const lines = [...stderrRaw.split(/\r?\n/), ...stdoutRaw.split(/\r?\n/)].map(
|
||||
(line) => line.trim(),
|
||||
const lines = [...stderrRaw.split(/\r?\n/), ...stdoutRaw.split(/\r?\n/)].map((line) =>
|
||||
line.trim(),
|
||||
);
|
||||
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
||||
const line = lines[i];
|
||||
@@ -39,7 +37,5 @@ export async function readLastGatewayErrorLine(
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return (
|
||||
(await readLastLogLine(stderrPath)) ?? (await readLastLogLine(stdoutPath))
|
||||
);
|
||||
return (await readLastLogLine(stderrPath)) ?? (await readLastLogLine(stdoutPath));
|
||||
}
|
||||
|
||||
@@ -29,19 +29,13 @@ const EXTRA_MARKERS = ["clawdbot", "clawdis"];
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export function renderGatewayServiceCleanupHints(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
||||
): string[] {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
switch (process.platform) {
|
||||
case "darwin": {
|
||||
const label = resolveGatewayLaunchAgentLabel(profile);
|
||||
return [
|
||||
`launchctl bootout gui/$UID/${label}`,
|
||||
`rm ~/Library/LaunchAgents/${label}.plist`,
|
||||
];
|
||||
return [`launchctl bootout gui/$UID/${label}`, `rm ~/Library/LaunchAgents/${label}.plist`];
|
||||
}
|
||||
case "linux": {
|
||||
const unit = resolveGatewaySystemdServiceName(profile);
|
||||
@@ -80,20 +74,14 @@ function hasGatewayServiceMarker(content: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isClawdbotGatewayLaunchdService(
|
||||
label: string,
|
||||
contents: string,
|
||||
): boolean {
|
||||
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 {
|
||||
function isClawdbotGatewaySystemdService(name: string, contents: string): boolean {
|
||||
if (hasGatewayServiceMarker(contents)) return true;
|
||||
if (!name.startsWith("clawdbot-gateway")) return false;
|
||||
return contents.toLowerCase().includes("gateway");
|
||||
@@ -103,23 +91,18 @@ 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")
|
||||
);
|
||||
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,
|
||||
);
|
||||
const match = contents.match(/<key>Label<\/key>\s*<string>([\s\S]*?)<\/string>/i);
|
||||
if (!match) return null;
|
||||
return match[1]?.trim() || null;
|
||||
}
|
||||
|
||||
function isIgnoredLaunchdLabel(label: string): boolean {
|
||||
return (
|
||||
label === resolveGatewayLaunchAgentLabel() ||
|
||||
LEGACY_GATEWAY_LAUNCH_AGENT_LABELS.includes(label)
|
||||
label === resolveGatewayLaunchAgentLabel() || LEGACY_GATEWAY_LAUNCH_AGENT_LABELS.includes(label)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,11 +248,7 @@ async function execSchtasks(
|
||||
return {
|
||||
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
||||
stderr:
|
||||
typeof e.stderr === "string"
|
||||
? e.stderr
|
||||
: typeof e.message === "string"
|
||||
? e.message
|
||||
: "",
|
||||
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
|
||||
code: typeof e.code === "number" ? e.code : 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ const plistUnescape = (value: string): string =>
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll("&", "&");
|
||||
|
||||
const renderEnvDict = (
|
||||
env: Record<string, string | undefined> | undefined,
|
||||
): string => {
|
||||
const renderEnvDict = (env: Record<string, string | undefined> | undefined): string => {
|
||||
if (!env) return "";
|
||||
const entries = Object.entries(env).filter(
|
||||
([, value]) => typeof value === "string" && value.trim(),
|
||||
@@ -33,9 +31,7 @@ const renderEnvDict = (
|
||||
return `\n <key>EnvironmentVariables</key>\n <dict>${items}\n </dict>`;
|
||||
};
|
||||
|
||||
export async function readLaunchAgentProgramArgumentsFromFile(
|
||||
plistPath: string,
|
||||
): Promise<{
|
||||
export async function readLaunchAgentProgramArgumentsFromFile(plistPath: string): Promise<{
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string>;
|
||||
@@ -43,22 +39,16 @@ export async function readLaunchAgentProgramArgumentsFromFile(
|
||||
} | null> {
|
||||
try {
|
||||
const plist = await fs.readFile(plistPath, "utf8");
|
||||
const programMatch = plist.match(
|
||||
/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i,
|
||||
);
|
||||
const programMatch = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i);
|
||||
if (!programMatch) return null;
|
||||
const args = Array.from(
|
||||
programMatch[1].matchAll(/<string>([\s\S]*?)<\/string>/gi),
|
||||
).map((match) => plistUnescape(match[1] ?? "").trim());
|
||||
const args = Array.from(programMatch[1].matchAll(/<string>([\s\S]*?)<\/string>/gi)).map(
|
||||
(match) => plistUnescape(match[1] ?? "").trim(),
|
||||
);
|
||||
const workingDirMatch = plist.match(
|
||||
/<key>WorkingDirectory<\/key>\s*<string>([\s\S]*?)<\/string>/i,
|
||||
);
|
||||
const workingDirectory = workingDirMatch
|
||||
? plistUnescape(workingDirMatch[1] ?? "").trim()
|
||||
: "";
|
||||
const envMatch = plist.match(
|
||||
/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/i,
|
||||
);
|
||||
const workingDirectory = workingDirMatch ? plistUnescape(workingDirMatch[1] ?? "").trim() : "";
|
||||
const envMatch = plist.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/i);
|
||||
const environment: Record<string, string> = {};
|
||||
if (envMatch) {
|
||||
for (const pair of envMatch[1].matchAll(
|
||||
|
||||
@@ -46,32 +46,23 @@ function resolveLaunchAgentPlistPathForLabel(
|
||||
return path.join(home, "Library", "LaunchAgents", `${label}.plist`);
|
||||
}
|
||||
|
||||
export function resolveLaunchAgentPlistPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
export function resolveLaunchAgentPlistPath(env: Record<string, string | undefined>): string {
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
return resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
}
|
||||
|
||||
export function resolveGatewayLogPaths(
|
||||
env: Record<string, string | undefined>,
|
||||
): {
|
||||
export function resolveGatewayLogPaths(env: Record<string, string | undefined>): {
|
||||
logDir: string;
|
||||
stdoutPath: string;
|
||||
stderrPath: string;
|
||||
} {
|
||||
const home = resolveHomeDir(env);
|
||||
const stateOverride =
|
||||
env.CLAWDBOT_STATE_DIR?.trim() || env.CLAWDIS_STATE_DIR?.trim();
|
||||
const stateOverride = env.CLAWDBOT_STATE_DIR?.trim() || env.CLAWDIS_STATE_DIR?.trim();
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
||||
const suffix =
|
||||
profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
const suffix = profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
const defaultStateDir = path.join(home, `.clawdbot${suffix}`);
|
||||
const stateDir = stateOverride
|
||||
? resolveUserPathWithHome(stateOverride, home)
|
||||
: defaultStateDir;
|
||||
const stateDir = stateOverride ? resolveUserPathWithHome(stateOverride, home) : defaultStateDir;
|
||||
const logDir = path.join(stateDir, "logs");
|
||||
return {
|
||||
logDir,
|
||||
@@ -152,11 +143,7 @@ async function execLaunchctl(
|
||||
return {
|
||||
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
||||
stderr:
|
||||
typeof e.stderr === "string"
|
||||
? e.stderr
|
||||
: typeof e.message === "string"
|
||||
? e.message
|
||||
: "",
|
||||
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
|
||||
code: typeof e.code === "number" ? e.code : 1,
|
||||
};
|
||||
}
|
||||
@@ -204,9 +191,7 @@ export async function isLaunchAgentLoaded(params?: {
|
||||
return res.code === 0;
|
||||
}
|
||||
|
||||
async function hasLaunchAgentPlist(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<boolean> {
|
||||
async function hasLaunchAgentPlist(env: Record<string, string | undefined>): Promise<boolean> {
|
||||
const plistPath = resolveLaunchAgentPlistPath(env);
|
||||
try {
|
||||
await fs.access(plistPath);
|
||||
@@ -221,8 +206,7 @@ export async function readLaunchAgentRuntime(
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
const domain = resolveGuiDomain();
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
const res = await execLaunchctl(["print", `${domain}/${label}`]);
|
||||
if (res.code !== 0) {
|
||||
return {
|
||||
@@ -234,12 +218,7 @@ export async function readLaunchAgentRuntime(
|
||||
const parsed = parseLaunchctlPrint(res.stdout || res.stderr || "");
|
||||
const plistExists = await hasLaunchAgentPlist(env);
|
||||
const state = parsed.state?.toLowerCase();
|
||||
const status =
|
||||
state === "running" || parsed.pid
|
||||
? "running"
|
||||
: state
|
||||
? "stopped"
|
||||
: "unknown";
|
||||
const status = state === "running" || parsed.pid ? "running" : state ? "stopped" : "unknown";
|
||||
return {
|
||||
status,
|
||||
state: parsed.state,
|
||||
@@ -312,13 +291,9 @@ export async function uninstallLegacyLaunchAgents({
|
||||
const dest = path.join(trashDir, `${agent.label}.plist`);
|
||||
try {
|
||||
await fs.rename(agent.plistPath, dest);
|
||||
stdout.write(
|
||||
`${formatLine("Moved legacy LaunchAgent to Trash", dest)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Moved legacy LaunchAgent to Trash", dest)}\n`);
|
||||
} catch {
|
||||
stdout.write(
|
||||
`Legacy LaunchAgent remains at ${agent.plistPath} (could not move)\n`,
|
||||
);
|
||||
stdout.write(`Legacy LaunchAgent remains at ${agent.plistPath} (could not move)\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,8 +322,7 @@ export async function uninstallLaunchAgent({
|
||||
const home = resolveHomeDir(env);
|
||||
const trashDir = path.join(home, ".Trash");
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
const dest = path.join(trashDir, `${label}.plist`);
|
||||
try {
|
||||
await fs.mkdir(trashDir, { recursive: true });
|
||||
@@ -359,11 +333,7 @@ export async function uninstallLaunchAgent({
|
||||
}
|
||||
}
|
||||
|
||||
function isLaunchctlNotLoaded(res: {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
}): boolean {
|
||||
function isLaunchctlNotLoaded(res: { stdout: string; stderr: string; code: number }): boolean {
|
||||
const detail = `${res.stderr || res.stdout}`.toLowerCase();
|
||||
return (
|
||||
detail.includes("no such process") ||
|
||||
@@ -385,9 +355,7 @@ export async function stopLaunchAgent({
|
||||
const label = resolveLaunchAgentLabel({ env, profile });
|
||||
const res = await execLaunchctl(["bootout", `${domain}/${label}`]);
|
||||
if (res.code !== 0 && !isLaunchctlNotLoaded(res)) {
|
||||
throw new Error(
|
||||
`launchctl bootout failed: ${res.stderr || res.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`launchctl bootout failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(`${formatLine("Stopped LaunchAgent", `${domain}/${label}`)}\n`);
|
||||
}
|
||||
@@ -410,13 +378,9 @@ export async function installLaunchAgent({
|
||||
|
||||
const domain = resolveGuiDomain();
|
||||
const label =
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() ||
|
||||
resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
|
||||
for (const legacyLabel of LEGACY_GATEWAY_LAUNCH_AGENT_LABELS) {
|
||||
const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(
|
||||
env,
|
||||
legacyLabel,
|
||||
);
|
||||
const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(env, legacyLabel);
|
||||
await execLaunchctl(["bootout", domain, legacyPlistPath]);
|
||||
await execLaunchctl(["unload", legacyPlistPath]);
|
||||
try {
|
||||
@@ -431,8 +395,7 @@ export async function installLaunchAgent({
|
||||
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
version: environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const plist = buildLaunchAgentPlist({
|
||||
label,
|
||||
@@ -449,9 +412,7 @@ export async function installLaunchAgent({
|
||||
await execLaunchctl(["unload", plistPath]);
|
||||
const boot = await execLaunchctl(["bootstrap", domain, plistPath]);
|
||||
if (boot.code !== 0) {
|
||||
throw new Error(
|
||||
`launchctl bootstrap failed: ${boot.stderr || boot.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`launchctl bootstrap failed: ${boot.stderr || boot.stdout}`.trim());
|
||||
}
|
||||
await execLaunchctl(["enable", `${domain}/${label}`]);
|
||||
await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
@@ -474,11 +435,7 @@ export async function restartLaunchAgent({
|
||||
const label = resolveLaunchAgentLabel({ env, profile });
|
||||
const res = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
`launchctl kickstart failed: ${res.stderr || res.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`launchctl kickstart failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(
|
||||
`${formatLine("Restarted LaunchAgent", `${domain}/${label}`)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Restarted LaunchAgent", `${domain}/${label}`)}\n`);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import {
|
||||
findLegacyLaunchAgents,
|
||||
uninstallLegacyLaunchAgents,
|
||||
} from "./launchd.js";
|
||||
import {
|
||||
findLegacyScheduledTasks,
|
||||
uninstallLegacyScheduledTasks,
|
||||
} from "./schtasks.js";
|
||||
import {
|
||||
findLegacySystemdUnits,
|
||||
uninstallLegacySystemdUnits,
|
||||
} from "./systemd.js";
|
||||
import { findLegacyLaunchAgents, uninstallLegacyLaunchAgents } from "./launchd.js";
|
||||
import { findLegacyScheduledTasks, uninstallLegacyScheduledTasks } from "./schtasks.js";
|
||||
import { findLegacySystemdUnits, uninstallLegacySystemdUnits } from "./systemd.js";
|
||||
|
||||
export type LegacyGatewayService = {
|
||||
platform: "darwin" | "linux" | "win32";
|
||||
|
||||
@@ -23,12 +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/clawdbot",
|
||||
);
|
||||
const entryPath = path.resolve(
|
||||
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js",
|
||||
);
|
||||
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot");
|
||||
const entryPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js");
|
||||
process.argv = ["node", argv1];
|
||||
fsMocks.realpath.mockResolvedValue(entryPath);
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
@@ -50,12 +46,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/clawdbot",
|
||||
);
|
||||
const indexPath = path.resolve(
|
||||
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js",
|
||||
);
|
||||
const argv1 = path.resolve("/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot");
|
||||
const indexPath = path.resolve("/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js");
|
||||
process.argv = ["node", argv1];
|
||||
fsMocks.realpath.mockRejectedValue(new Error("no realpath"));
|
||||
fsMocks.access.mockImplementation(async (target: string) => {
|
||||
|
||||
@@ -69,11 +69,7 @@ function buildDistCandidates(...inputs: string[]): string[] {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function appendDistCandidates(
|
||||
candidates: string[],
|
||||
seen: Set<string>,
|
||||
baseDir: string,
|
||||
): void {
|
||||
function appendDistCandidates(candidates: string[], seen: Set<string>, baseDir: string): void {
|
||||
const distDir = path.resolve(baseDir, "dist");
|
||||
const distEntries = [
|
||||
path.join(distDir, "index.js"),
|
||||
@@ -154,8 +150,7 @@ export async function resolveGatewayProgramArguments(params: {
|
||||
|
||||
if (runtime === "node") {
|
||||
const nodePath =
|
||||
params.nodePath ??
|
||||
(isNodeRuntime(execPath) ? execPath : await resolveNodePath());
|
||||
params.nodePath ?? (isNodeRuntime(execPath) ? execPath : await resolveNodePath());
|
||||
const cliEntrypointPath = await resolveCliEntrypointPathForService();
|
||||
return {
|
||||
programArguments: [nodePath, cliEntrypointPath, ...gatewayArgs],
|
||||
@@ -167,9 +162,7 @@ export async function resolveGatewayProgramArguments(params: {
|
||||
const repoRoot = resolveRepoRootForDev();
|
||||
const devCliPath = path.join(repoRoot, "src", "index.ts");
|
||||
await fs.access(devCliPath);
|
||||
const bunPath = isBunRuntime(execPath)
|
||||
? execPath
|
||||
: await resolveBunPath();
|
||||
const bunPath = isBunRuntime(execPath) ? execPath : await resolveBunPath();
|
||||
return {
|
||||
programArguments: [bunPath, devCliPath, ...gatewayArgs],
|
||||
workingDirectory: repoRoot,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export function parseKeyValueOutput(
|
||||
output: string,
|
||||
separator: string,
|
||||
): Record<string, string> {
|
||||
export function parseKeyValueOutput(output: string, separator: string): Record<string, string> {
|
||||
const entries: Record<string, string> = {};
|
||||
for (const rawLine of output.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
@@ -28,9 +28,7 @@ describe("resolvePreferredNodePath", () => {
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
const execFile = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ stdout: "22.1.0\n", stderr: "" });
|
||||
const execFile = vi.fn().mockResolvedValue({ stdout: "22.1.0\n", stderr: "" });
|
||||
|
||||
const result = await resolvePreferredNodePath({
|
||||
env: {},
|
||||
@@ -49,9 +47,7 @@ describe("resolvePreferredNodePath", () => {
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
const execFile = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ stdout: "18.19.0\n", stderr: "" });
|
||||
const execFile = vi.fn().mockResolvedValue({ stdout: "18.19.0\n", stderr: "" });
|
||||
|
||||
const result = await resolvePreferredNodePath({
|
||||
env: {},
|
||||
@@ -90,9 +86,7 @@ describe("resolveSystemNodeInfo", () => {
|
||||
throw new Error("missing");
|
||||
});
|
||||
|
||||
const execFile = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ stdout: "22.0.0\n", stderr: "" });
|
||||
const execFile = vi.fn().mockResolvedValue({ stdout: "22.0.0\n", stderr: "" });
|
||||
|
||||
const result = await resolveSystemNodeInfo({
|
||||
env: {},
|
||||
|
||||
@@ -42,8 +42,7 @@ function buildSystemNodeCandidates(
|
||||
if (platform === "win32") {
|
||||
const pathModule = getPathModule(platform);
|
||||
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
||||
const programFilesX86 =
|
||||
env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
||||
const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
||||
return [
|
||||
pathModule.join(programFiles, "nodejs", "node.exe"),
|
||||
pathModule.join(programFilesX86, "nodejs", "node.exe"),
|
||||
@@ -65,11 +64,9 @@ async function resolveNodeVersion(
|
||||
execFileImpl: ExecFileAsync,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execFileImpl(
|
||||
nodePath,
|
||||
["-p", "process.versions.node"],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const { stdout } = await execFileImpl(nodePath, ["-p", "process.versions.node"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const value = stdout.trim();
|
||||
return value ? value : null;
|
||||
} catch {
|
||||
@@ -129,10 +126,7 @@ export async function resolveSystemNodeInfo(params: {
|
||||
const systemNode = await resolveSystemNodePath(env, platform);
|
||||
if (!systemNode) return null;
|
||||
|
||||
const version = await resolveNodeVersion(
|
||||
systemNode,
|
||||
params.execFile ?? execFileAsync,
|
||||
);
|
||||
const version = await resolveNodeVersion(systemNode, params.execFile ?? execFileAsync);
|
||||
return {
|
||||
path: systemNode,
|
||||
version,
|
||||
@@ -146,9 +140,7 @@ export function renderSystemNodeWarning(
|
||||
): string | null {
|
||||
if (!systemNode || systemNode.supported) return null;
|
||||
const versionLabel = systemNode.version ?? "unknown";
|
||||
const selectedLabel = selectedNodePath
|
||||
? ` Using ${selectedNodePath} for the daemon.`
|
||||
: "";
|
||||
const selectedLabel = selectedNodePath ? ` Using ${selectedNodePath} for the daemon.` : "";
|
||||
return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22+.${selectedLabel} Install Node 22+ from nodejs.org or Homebrew.`;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,19 +25,14 @@ function resolveHomeDir(env: Record<string, string | undefined>): string {
|
||||
return home;
|
||||
}
|
||||
|
||||
function resolveTaskScriptPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
function resolveTaskScriptPath(env: Record<string, string | undefined>): string {
|
||||
const home = resolveHomeDir(env);
|
||||
const profile = env.CLAWDBOT_PROFILE?.trim();
|
||||
const suffix =
|
||||
profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
const suffix = profile && profile.toLowerCase() !== "default" ? `-${profile}` : "";
|
||||
return path.join(home, `.clawdbot${suffix}`, "gateway.cmd");
|
||||
}
|
||||
|
||||
function resolveLegacyTaskScriptPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
function resolveLegacyTaskScriptPath(env: Record<string, string | undefined>): string {
|
||||
const home = resolveHomeDir(env);
|
||||
return path.join(home, ".clawdis", "gateway.cmd");
|
||||
}
|
||||
@@ -80,9 +75,7 @@ function parseCommandLine(value: string): string[] {
|
||||
return args;
|
||||
}
|
||||
|
||||
export async function readScheduledTaskCommand(
|
||||
env: Record<string, string | undefined>,
|
||||
): Promise<{
|
||||
export async function readScheduledTaskCommand(env: Record<string, string | undefined>): Promise<{
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string>;
|
||||
@@ -109,10 +102,7 @@ export async function readScheduledTaskCommand(
|
||||
continue;
|
||||
}
|
||||
if (line.toLowerCase().startsWith("cd /d ")) {
|
||||
workingDirectory = line
|
||||
.slice("cd /d ".length)
|
||||
.trim()
|
||||
.replace(/^"|"$/g, "");
|
||||
workingDirectory = line.slice("cd /d ".length).trim().replace(/^"|"$/g, "");
|
||||
continue;
|
||||
}
|
||||
commandLine = line;
|
||||
@@ -199,11 +189,7 @@ async function execSchtasks(
|
||||
return {
|
||||
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
||||
stderr:
|
||||
typeof e.stderr === "string"
|
||||
? e.stderr
|
||||
: typeof e.message === "string"
|
||||
? e.message
|
||||
: "",
|
||||
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
|
||||
code: typeof e.code === "number" ? e.code : 1,
|
||||
};
|
||||
}
|
||||
@@ -234,8 +220,7 @@ export async function installScheduledTask({
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
version: environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const script = buildTaskScript({
|
||||
description,
|
||||
@@ -260,9 +245,7 @@ export async function installScheduledTask({
|
||||
quotedScript,
|
||||
]);
|
||||
if (create.code !== 0) {
|
||||
throw new Error(
|
||||
`schtasks create failed: ${create.stderr || create.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`schtasks create failed: ${create.stderr || create.stdout}`.trim());
|
||||
}
|
||||
|
||||
await execSchtasks(["/Run", "/TN", taskName]);
|
||||
@@ -291,11 +274,7 @@ export async function uninstallScheduledTask({
|
||||
}
|
||||
}
|
||||
|
||||
function isTaskNotRunning(res: {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
}): boolean {
|
||||
function isTaskNotRunning(res: { stdout: string; stderr: string; code: number }): boolean {
|
||||
const detail = `${res.stderr || res.stdout}`.toLowerCase();
|
||||
return detail.includes("not running");
|
||||
}
|
||||
@@ -333,9 +312,7 @@ export async function restartScheduledTask({
|
||||
stdout.write(`${formatLine("Restarted Scheduled Task", taskName)}\n`);
|
||||
}
|
||||
|
||||
export async function isScheduledTaskInstalled(
|
||||
profile?: string,
|
||||
): Promise<boolean> {
|
||||
export async function isScheduledTaskInstalled(profile?: string): Promise<boolean> {
|
||||
await assertSchtasksAvailable();
|
||||
const taskName = resolveGatewayWindowsTaskName(profile);
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName]);
|
||||
@@ -343,10 +320,7 @@ export async function isScheduledTaskInstalled(
|
||||
}
|
||||
|
||||
export async function readScheduledTaskRuntime(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
try {
|
||||
await assertSchtasksAvailable();
|
||||
@@ -357,14 +331,7 @@ export async function readScheduledTaskRuntime(
|
||||
};
|
||||
}
|
||||
const taskName = resolveGatewayWindowsTaskName(env.CLAWDBOT_PROFILE);
|
||||
const res = await execSchtasks([
|
||||
"/Query",
|
||||
"/TN",
|
||||
taskName,
|
||||
"/V",
|
||||
"/FO",
|
||||
"LIST",
|
||||
]);
|
||||
const res = await execSchtasks(["/Query", "/TN", taskName, "/V", "/FO", "LIST"]);
|
||||
if (res.code !== 0) {
|
||||
const detail = (res.stderr || res.stdout).trim();
|
||||
const missing = detail.toLowerCase().includes("cannot find the file");
|
||||
@@ -376,8 +343,7 @@ export async function readScheduledTaskRuntime(
|
||||
}
|
||||
const parsed = parseSchtasksQuery(res.stdout || "");
|
||||
const statusRaw = parsed.status?.toLowerCase();
|
||||
const status =
|
||||
statusRaw === "running" ? "running" : statusRaw ? "stopped" : "unknown";
|
||||
const status = statusRaw === "running" ? "running" : statusRaw ? "stopped" : "unknown";
|
||||
return {
|
||||
status,
|
||||
state: parsed.status,
|
||||
@@ -446,16 +412,12 @@ export async function uninstallLegacyScheduledTasks({
|
||||
if (schtasksAvailable && task.installed) {
|
||||
await execSchtasks(["/Delete", "/F", "/TN", task.name]);
|
||||
} else if (!schtasksAvailable && task.installed) {
|
||||
stdout.write(
|
||||
`schtasks unavailable; unable to remove legacy task: ${task.name}\n`,
|
||||
);
|
||||
stdout.write(`schtasks unavailable; unable to remove legacy task: ${task.name}\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.unlink(task.scriptPath);
|
||||
stdout.write(
|
||||
`${formatLine("Removed legacy task script", task.scriptPath)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Removed legacy task script", task.scriptPath)}\n`);
|
||||
} catch {
|
||||
stdout.write(`Legacy task script not found at ${task.scriptPath}\n`);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
auditGatewayServiceConfig,
|
||||
SERVICE_AUDIT_CODES,
|
||||
} from "./service-audit.js";
|
||||
import { auditGatewayServiceConfig, SERVICE_AUDIT_CODES } from "./service-audit.js";
|
||||
|
||||
describe("auditGatewayServiceConfig", () => {
|
||||
it("flags bun runtime", async () => {
|
||||
@@ -14,11 +11,9 @@ describe("auditGatewayServiceConfig", () => {
|
||||
environment: { PATH: "/usr/bin:/bin" },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
audit.issues.some(
|
||||
(issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeBun,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeBun)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("flags version-managed node paths", async () => {
|
||||
@@ -26,10 +21,7 @@ describe("auditGatewayServiceConfig", () => {
|
||||
env: { HOME: "/tmp" },
|
||||
platform: "darwin",
|
||||
command: {
|
||||
programArguments: [
|
||||
"/Users/test/.nvm/versions/node/v22.0.0/bin/node",
|
||||
"gateway",
|
||||
],
|
||||
programArguments: ["/Users/test/.nvm/versions/node/v22.0.0/bin/node", "gateway"],
|
||||
environment: {
|
||||
PATH: "/usr/bin:/bin:/Users/test/.nvm/versions/node/v22.0.0/bin",
|
||||
},
|
||||
@@ -37,19 +29,14 @@ describe("auditGatewayServiceConfig", () => {
|
||||
});
|
||||
expect(
|
||||
audit.issues.some(
|
||||
(issue) =>
|
||||
issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager,
|
||||
(issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
audit.issues.some(
|
||||
(issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathNonMinimal,
|
||||
),
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathNonMinimal),
|
||||
).toBe(true);
|
||||
expect(
|
||||
audit.issues.some(
|
||||
(issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs,
|
||||
),
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,9 +44,7 @@ export const SERVICE_AUDIT_CODES = {
|
||||
systemdWantsNetworkOnline: "systemd-wants-network-online",
|
||||
} as const;
|
||||
|
||||
export function needsNodeRuntimeMigration(
|
||||
issues: ServiceConfigIssue[],
|
||||
): boolean {
|
||||
export function needsNodeRuntimeMigration(issues: ServiceConfigIssue[]): boolean {
|
||||
return issues.some(
|
||||
(issue) =>
|
||||
issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeBun ||
|
||||
@@ -171,10 +169,7 @@ async function auditLaunchdPlist(
|
||||
}
|
||||
}
|
||||
|
||||
function auditGatewayCommand(
|
||||
programArguments: string[] | undefined,
|
||||
issues: ServiceConfigIssue[],
|
||||
) {
|
||||
function auditGatewayCommand(programArguments: string[] | undefined, issues: ServiceConfigIssue[]) {
|
||||
if (!programArguments || programArguments.length === 0) return;
|
||||
if (!hasGatewaySubcommand(programArguments)) {
|
||||
issues.push({
|
||||
@@ -218,8 +213,7 @@ function auditGatewayServicePath(
|
||||
if (!servicePath) {
|
||||
issues.push({
|
||||
code: SERVICE_AUDIT_CODES.gatewayPathMissing,
|
||||
message:
|
||||
"Gateway service PATH is not set; the daemon should use a minimal PATH.",
|
||||
message: "Gateway service PATH is not set; the daemon should use a minimal PATH.",
|
||||
level: "recommended",
|
||||
});
|
||||
return;
|
||||
@@ -230,9 +224,7 @@ function auditGatewayServicePath(
|
||||
.split(getPathModule(platform).delimiter)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
const normalizedParts = parts.map((entry) =>
|
||||
normalizePathEntry(entry, platform),
|
||||
);
|
||||
const normalizedParts = parts.map((entry) => normalizePathEntry(entry, platform));
|
||||
const missing = expected.filter((entry) => {
|
||||
const normalized = normalizePathEntry(entry, platform);
|
||||
return !normalizedParts.includes(normalized);
|
||||
@@ -284,8 +276,7 @@ async function auditGatewayRuntime(
|
||||
if (isBunRuntime(execPath)) {
|
||||
issues.push({
|
||||
code: SERVICE_AUDIT_CODES.gatewayRuntimeBun,
|
||||
message:
|
||||
"Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram channels.",
|
||||
message: "Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram channels.",
|
||||
detail: execPath,
|
||||
level: "recommended",
|
||||
});
|
||||
@@ -297,8 +288,7 @@ async function auditGatewayRuntime(
|
||||
if (isVersionManagedNodePath(execPath, platform)) {
|
||||
issues.push({
|
||||
code: SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager,
|
||||
message:
|
||||
"Gateway service uses Node from a version manager; it can break after upgrades.",
|
||||
message: "Gateway service uses Node from a version manager; it can break after upgrades.",
|
||||
detail: execPath,
|
||||
level: "recommended",
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildMinimalServicePath,
|
||||
buildServiceEnvironment,
|
||||
} from "./service-env.js";
|
||||
import { buildMinimalServicePath, buildServiceEnvironment } from "./service-env.js";
|
||||
|
||||
describe("buildMinimalServicePath", () => {
|
||||
it("includes Homebrew + system dirs on macOS", () => {
|
||||
|
||||
@@ -27,9 +27,7 @@ function resolveSystemPathDirs(platform: NodeJS.Platform): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getMinimalServicePathParts(
|
||||
options: MinimalServicePathOptions = {},
|
||||
): string[] {
|
||||
export function getMinimalServicePathParts(options: MinimalServicePathOptions = {}): string[] {
|
||||
const platform = options.platform ?? process.platform;
|
||||
if (platform === "win32") return [];
|
||||
|
||||
@@ -48,9 +46,7 @@ export function getMinimalServicePathParts(
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function buildMinimalServicePath(
|
||||
options: BuildServicePathOptions = {},
|
||||
): string {
|
||||
export function buildMinimalServicePath(options: BuildServicePathOptions = {}): string {
|
||||
const env = options.env ?? process.env;
|
||||
const platform = options.platform ?? process.platform;
|
||||
if (platform === "win32") {
|
||||
@@ -70,9 +66,7 @@ export function buildServiceEnvironment(params: {
|
||||
const profile = env.CLAWDBOT_PROFILE;
|
||||
const resolvedLaunchdLabel =
|
||||
launchdLabel ||
|
||||
(process.platform === "darwin"
|
||||
? resolveGatewayLaunchAgentLabel(profile)
|
||||
: undefined);
|
||||
(process.platform === "darwin" ? resolveGatewayLaunchAgentLabel(profile) : undefined);
|
||||
const systemdUnit = `${resolveGatewaySystemdServiceName(profile)}.service`;
|
||||
return {
|
||||
PATH: buildMinimalServicePath({ env }),
|
||||
|
||||
@@ -64,9 +64,7 @@ export type GatewayService = {
|
||||
environment?: Record<string, string>;
|
||||
sourcePath?: string;
|
||||
} | null>;
|
||||
readRuntime: (
|
||||
env: Record<string, string | undefined>,
|
||||
) => Promise<GatewayServiceRuntime>;
|
||||
readRuntime: (env: Record<string, string | undefined>) => Promise<GatewayServiceRuntime>;
|
||||
};
|
||||
|
||||
export function resolveGatewayService(): GatewayService {
|
||||
@@ -95,8 +93,7 @@ export function resolveGatewayService(): GatewayService {
|
||||
env: args.env,
|
||||
});
|
||||
},
|
||||
isLoaded: async (args) =>
|
||||
isLaunchAgentLoaded({ profile: args.profile, env: args.env }),
|
||||
isLoaded: async (args) => isLaunchAgentLoaded({ profile: args.profile, env: args.env }),
|
||||
readCommand: readLaunchAgentProgramArguments,
|
||||
readRuntime: readLaunchAgentRuntime,
|
||||
};
|
||||
@@ -127,8 +124,7 @@ export function resolveGatewayService(): GatewayService {
|
||||
env: args.env,
|
||||
});
|
||||
},
|
||||
isLoaded: async (args) =>
|
||||
isSystemdServiceEnabled({ profile: args.profile, env: args.env }),
|
||||
isLoaded: async (args) => isSystemdServiceEnabled({ profile: args.profile, env: args.env }),
|
||||
readCommand: readSystemdServiceExecStart,
|
||||
readRuntime: async (env) => await readSystemdServiceRuntime(env),
|
||||
};
|
||||
@@ -163,7 +159,5 @@ export function resolveGatewayService(): GatewayService {
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Gateway service install not supported on ${process.platform}`,
|
||||
);
|
||||
throw new Error(`Gateway service install not supported on ${process.platform}`);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import os from "node:os";
|
||||
import { runCommandWithTimeout, runExec } from "../process/exec.js";
|
||||
|
||||
function resolveLoginctlUser(
|
||||
env: Record<string, string | undefined>,
|
||||
): string | null {
|
||||
function resolveLoginctlUser(env: Record<string, string | undefined>): string | null {
|
||||
const fromEnv = env.USER?.trim() || env.LOGNAME?.trim();
|
||||
if (fromEnv) return fromEnv;
|
||||
try {
|
||||
@@ -24,11 +22,9 @@ export async function readSystemdUserLingerStatus(
|
||||
const user = resolveLoginctlUser(env);
|
||||
if (!user) return null;
|
||||
try {
|
||||
const { stdout } = await runExec(
|
||||
"loginctl",
|
||||
["show-user", user, "-p", "Linger"],
|
||||
{ timeoutMs: 5_000 },
|
||||
);
|
||||
const { stdout } = await runExec("loginctl", ["show-user", user, "-p", "Linger"], {
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
const line = stdout
|
||||
.split("\n")
|
||||
.map((entry) => entry.trim())
|
||||
@@ -52,8 +48,7 @@ export async function enableSystemdUserLinger(params: {
|
||||
if (!user) {
|
||||
return { ok: false, stdout: "", stderr: "Missing user", code: 1 };
|
||||
}
|
||||
const needsSudo =
|
||||
typeof process.getuid === "function" ? process.getuid() !== 0 : true;
|
||||
const needsSudo = typeof process.getuid === "function" ? process.getuid() !== 0 : true;
|
||||
const sudoArgs =
|
||||
needsSudo && params.sudoMode !== undefined
|
||||
? ["sudo", ...(params.sudoMode === "non-interactive" ? ["-n"] : [])]
|
||||
|
||||
@@ -3,17 +3,14 @@ function systemdEscapeArg(value: string): string {
|
||||
return `"${value.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"')}"`;
|
||||
}
|
||||
|
||||
function renderEnvLines(
|
||||
env: Record<string, string | undefined> | undefined,
|
||||
): string[] {
|
||||
function renderEnvLines(env: Record<string, string | undefined> | undefined): string[] {
|
||||
if (!env) return [];
|
||||
const entries = Object.entries(env).filter(
|
||||
([, value]) => typeof value === "string" && value.trim(),
|
||||
);
|
||||
if (entries.length === 0) return [];
|
||||
return entries.map(
|
||||
([key, value]) =>
|
||||
`Environment=${systemdEscapeArg(`${key}=${value?.trim() ?? ""}`)}`,
|
||||
([key, value]) => `Environment=${systemdEscapeArg(`${key}=${value?.trim() ?? ""}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,9 +89,7 @@ export function parseSystemdExecStart(value: string): string[] {
|
||||
return args;
|
||||
}
|
||||
|
||||
export function parseSystemdEnvAssignment(
|
||||
raw: string,
|
||||
): { key: string; value: string } | null {
|
||||
export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
|
||||
@@ -42,14 +42,10 @@ function resolveSystemdUnitPathForName(
|
||||
return path.join(home, ".config", "systemd", "user", `${name}.service`);
|
||||
}
|
||||
|
||||
function resolveSystemdServiceName(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
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 override.endsWith(".service") ? override.slice(0, -".service".length) : override;
|
||||
}
|
||||
return resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
|
||||
}
|
||||
@@ -62,15 +58,11 @@ function resolveSystemdServiceNameFromParams(params?: {
|
||||
return resolveGatewaySystemdServiceName(params?.profile);
|
||||
}
|
||||
|
||||
function resolveSystemdUnitPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
function resolveSystemdUnitPath(env: Record<string, string | undefined>): string {
|
||||
return resolveSystemdUnitPathForName(env, resolveSystemdServiceName(env));
|
||||
}
|
||||
|
||||
export function resolveSystemdUserUnitPath(
|
||||
env: Record<string, string | undefined>,
|
||||
): string {
|
||||
export function resolveSystemdUserUnitPath(env: Record<string, string | undefined>): string {
|
||||
return resolveSystemdUnitPath(env);
|
||||
}
|
||||
|
||||
@@ -171,11 +163,7 @@ async function execSystemctl(
|
||||
return {
|
||||
stdout: typeof e.stdout === "string" ? e.stdout : "",
|
||||
stderr:
|
||||
typeof e.stderr === "string"
|
||||
? e.stderr
|
||||
: typeof e.message === "string"
|
||||
? e.message
|
||||
: "",
|
||||
typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "",
|
||||
code: typeof e.code === "number" ? e.code : 1,
|
||||
};
|
||||
}
|
||||
@@ -199,13 +187,9 @@ async function assertSystemdAvailable() {
|
||||
if (res.code === 0) return;
|
||||
const detail = res.stderr || res.stdout;
|
||||
if (detail.toLowerCase().includes("not found")) {
|
||||
throw new Error(
|
||||
"systemctl not available; systemd user services are required on Linux.",
|
||||
);
|
||||
throw new Error("systemctl not available; systemd user services are required on Linux.");
|
||||
}
|
||||
throw new Error(
|
||||
`systemctl --user unavailable: ${detail || "unknown error"}`.trim(),
|
||||
);
|
||||
throw new Error(`systemctl --user unavailable: ${detail || "unknown error"}`.trim());
|
||||
}
|
||||
|
||||
export async function installSystemdService({
|
||||
@@ -227,8 +211,7 @@ export async function installSystemdService({
|
||||
await fs.mkdir(path.dirname(unitPath), { recursive: true });
|
||||
const description = formatGatewayServiceDescription({
|
||||
profile: env.CLAWDBOT_PROFILE,
|
||||
version:
|
||||
environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
version: environment?.CLAWDBOT_SERVICE_VERSION ?? env.CLAWDBOT_SERVICE_VERSION,
|
||||
});
|
||||
const unit = buildSystemdUnit({
|
||||
description,
|
||||
@@ -242,23 +225,17 @@ export async function installSystemdService({
|
||||
const unitName = `${serviceName}.service`;
|
||||
const reload = await execSystemctl(["--user", "daemon-reload"]);
|
||||
if (reload.code !== 0) {
|
||||
throw new Error(
|
||||
`systemctl daemon-reload failed: ${reload.stderr || reload.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`systemctl daemon-reload failed: ${reload.stderr || reload.stdout}`.trim());
|
||||
}
|
||||
|
||||
const enable = await execSystemctl(["--user", "enable", unitName]);
|
||||
if (enable.code !== 0) {
|
||||
throw new Error(
|
||||
`systemctl enable failed: ${enable.stderr || enable.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`systemctl enable failed: ${enable.stderr || enable.stdout}`.trim());
|
||||
}
|
||||
|
||||
const restart = await execSystemctl(["--user", "restart", unitName]);
|
||||
if (restart.code !== 0) {
|
||||
throw new Error(
|
||||
`systemctl restart failed: ${restart.stderr || restart.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`systemctl restart failed: ${restart.stderr || restart.stdout}`.trim());
|
||||
}
|
||||
|
||||
stdout.write(`${formatLine("Installed systemd service", unitPath)}\n`);
|
||||
@@ -300,9 +277,7 @@ export async function stopSystemdService({
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "stop", unitName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
`systemctl stop failed: ${res.stderr || res.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`systemctl stop failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(`${formatLine("Stopped systemd service", unitName)}\n`);
|
||||
}
|
||||
@@ -321,9 +296,7 @@ export async function restartSystemdService({
|
||||
const unitName = `${serviceName}.service`;
|
||||
const res = await execSystemctl(["--user", "restart", unitName]);
|
||||
if (res.code !== 0) {
|
||||
throw new Error(
|
||||
`systemctl restart failed: ${res.stderr || res.stdout}`.trim(),
|
||||
);
|
||||
throw new Error(`systemctl restart failed: ${res.stderr || res.stdout}`.trim());
|
||||
}
|
||||
stdout.write(`${formatLine("Restarted systemd service", unitName)}\n`);
|
||||
}
|
||||
@@ -340,10 +313,7 @@ export async function isSystemdServiceEnabled(params?: {
|
||||
}
|
||||
|
||||
export async function readSystemdServiceRuntime(
|
||||
env: Record<string, string | undefined> = process.env as Record<
|
||||
string,
|
||||
string | undefined
|
||||
>,
|
||||
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
||||
): Promise<GatewayServiceRuntime> {
|
||||
try {
|
||||
await assertSystemdAvailable();
|
||||
@@ -374,8 +344,7 @@ export async function readSystemdServiceRuntime(
|
||||
}
|
||||
const parsed = parseSystemdShow(res.stdout || "");
|
||||
const activeState = parsed.activeState?.toLowerCase();
|
||||
const status =
|
||||
activeState === "active" ? "running" : activeState ? "stopped" : "unknown";
|
||||
const status = activeState === "active" ? "running" : activeState ? "stopped" : "unknown";
|
||||
return {
|
||||
status,
|
||||
state: parsed.activeState,
|
||||
@@ -415,11 +384,7 @@ export async function findLegacySystemdUnits(
|
||||
}
|
||||
let enabled = false;
|
||||
if (systemctlAvailable) {
|
||||
const res = await execSystemctl([
|
||||
"--user",
|
||||
"is-enabled",
|
||||
`${name}.service`,
|
||||
]);
|
||||
const res = await execSystemctl(["--user", "is-enabled", `${name}.service`]);
|
||||
enabled = res.code === 0;
|
||||
}
|
||||
if (exists || enabled) {
|
||||
@@ -442,23 +407,14 @@ export async function uninstallLegacySystemdUnits({
|
||||
const systemctlAvailable = await isSystemctlAvailable();
|
||||
for (const unit of units) {
|
||||
if (systemctlAvailable) {
|
||||
await execSystemctl([
|
||||
"--user",
|
||||
"disable",
|
||||
"--now",
|
||||
`${unit.name}.service`,
|
||||
]);
|
||||
await execSystemctl(["--user", "disable", "--now", `${unit.name}.service`]);
|
||||
} else {
|
||||
stdout.write(
|
||||
`systemctl unavailable; removed legacy unit file only: ${unit.name}.service\n`,
|
||||
);
|
||||
stdout.write(`systemctl unavailable; removed legacy unit file only: ${unit.name}.service\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.unlink(unit.unitPath);
|
||||
stdout.write(
|
||||
`${formatLine("Removed legacy systemd service", unit.unitPath)}\n`,
|
||||
);
|
||||
stdout.write(`${formatLine("Removed legacy systemd service", unit.unitPath)}\n`);
|
||||
} catch {
|
||||
stdout.write(`Legacy systemd unit not found at ${unit.unitPath}\n`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user