chore: migrate to oxlint and oxfmt

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-14 14:31:43 +00:00
parent 912ebffc63
commit c379191f80
1480 changed files with 28608 additions and 43547 deletions

View File

@@ -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)",
);
});
});

View File

@@ -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));
}

View File

@@ -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,
};
}

View File

@@ -16,9 +16,7 @@ const plistUnescape = (value: string): string =>
.replaceAll("&lt;", "<")
.replaceAll("&amp;", "&");
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(

View File

@@ -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`);
}

View File

@@ -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";

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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();

View File

@@ -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: {},

View File

@@ -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.`;
}

View File

@@ -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`);
}

View File

@@ -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);
});
});

View File

@@ -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",
});

View File

@@ -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", () => {

View File

@@ -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 }),

View File

@@ -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}`);
}

View File

@@ -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"] : [])]

View File

@@ -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;

View File

@@ -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`);
}