chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -16,7 +16,9 @@ export const LEGACY_GATEWAY_WINDOWS_TASK_NAMES: string[] = [];
export function normalizeGatewayProfile(profile?: string): string | null {
const trimmed = profile?.trim();
if (!trimmed || trimmed.toLowerCase() === "default") return null;
if (!trimmed || trimmed.toLowerCase() === "default") {
return null;
}
return trimmed;
}
@@ -40,13 +42,17 @@ export function resolveLegacyGatewayLaunchAgentLabels(profile?: string): string[
export function resolveGatewaySystemdServiceName(profile?: string): string {
const suffix = resolveGatewayProfileSuffix(profile);
if (!suffix) return GATEWAY_SYSTEMD_SERVICE_NAME;
if (!suffix) {
return GATEWAY_SYSTEMD_SERVICE_NAME;
}
return `openclaw-gateway${suffix}`;
}
export function resolveGatewayWindowsTaskName(profile?: string): string {
const normalized = normalizeGatewayProfile(profile);
if (!normalized) return GATEWAY_WINDOWS_TASK_NAME;
if (!normalized) {
return GATEWAY_WINDOWS_TASK_NAME;
}
return `OpenClaw Gateway (${normalized})`;
}
@@ -57,9 +63,15 @@ export function formatGatewayServiceDescription(params?: {
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 "OpenClaw Gateway";
if (profile) {
parts.push(`profile: ${profile}`);
}
if (version) {
parts.push(`v${version}`);
}
if (parts.length === 0) {
return "OpenClaw Gateway";
}
return `OpenClaw Gateway (${parts.join(", ")})`;
}
@@ -77,6 +89,8 @@ export function resolveNodeWindowsTaskName(): string {
export function formatNodeServiceDescription(params?: { version?: string }): string {
const version = params?.version?.trim();
if (!version) return "OpenClaw Node Host";
if (!version) {
return "OpenClaw Node Host";
}
return `OpenClaw Node Host (v${version})`;
}

View File

@@ -15,7 +15,9 @@ async function readLastLogLine(filePath: string): Promise<string | null> {
const raw = await fs.readFile(filePath, "utf8");
const lines = raw.split(/\r?\n/).map((line) => line.trim());
for (let i = lines.length - 1; i >= 0; i -= 1) {
if (lines[i]) return lines[i];
if (lines[i]) {
return lines[i];
}
}
return null;
} catch {
@@ -32,7 +34,9 @@ export async function readLastGatewayErrorLine(env: NodeJS.ProcessEnv): Promise<
);
for (let i = lines.length - 1; i >= 0; i -= 1) {
const line = lines[i];
if (!line) continue;
if (!line) {
continue;
}
if (GATEWAY_LOG_ERROR_PATTERNS.some((pattern) => pattern.test(line))) {
return line;
}

View File

@@ -54,7 +54,9 @@ export function renderGatewayServiceCleanupHints(
function resolveHomeDir(env: Record<string, string | undefined>): string {
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
if (!home) throw new Error("Missing HOME");
if (!home) {
throw new Error("Missing HOME");
}
return home;
}
@@ -63,7 +65,9 @@ type Marker = (typeof EXTRA_MARKERS)[number];
function detectMarker(content: string): Marker | null {
const lower = content.toLowerCase();
for (const marker of EXTRA_MARKERS) {
if (lower.includes(marker)) return marker;
if (lower.includes(marker)) {
return marker;
}
}
return null;
}
@@ -85,28 +89,40 @@ function hasGatewayServiceMarker(content: string): boolean {
}
function isOpenClawGatewayLaunchdService(label: string, contents: string): boolean {
if (hasGatewayServiceMarker(contents)) return true;
if (hasGatewayServiceMarker(contents)) {
return true;
}
const lowerContents = contents.toLowerCase();
if (!lowerContents.includes("gateway")) return false;
if (!lowerContents.includes("gateway")) {
return false;
}
return label.startsWith("ai.openclaw.");
}
function isOpenClawGatewaySystemdService(name: string, contents: string): boolean {
if (hasGatewayServiceMarker(contents)) return true;
if (!name.startsWith("openclaw-gateway")) return false;
if (hasGatewayServiceMarker(contents)) {
return true;
}
if (!name.startsWith("openclaw-gateway")) {
return false;
}
return contents.toLowerCase().includes("gateway");
}
function isOpenClawGatewayTaskName(name: string): boolean {
const normalized = name.trim().toLowerCase();
if (!normalized) return false;
if (!normalized) {
return false;
}
const defaultName = resolveGatewayWindowsTaskName().toLowerCase();
return normalized === defaultName || normalized.startsWith("openclaw gateway");
}
function tryExtractPlistLabel(contents: string): string | null {
const match = contents.match(/<key>Label<\/key>\s*<string>([\s\S]*?)<\/string>/i);
if (!match) return null;
if (!match) {
return null;
}
return match[1]?.trim() || null;
}
@@ -136,9 +152,13 @@ async function scanLaunchdDir(params: {
}
for (const entry of entries) {
if (!entry.endsWith(".plist")) continue;
if (!entry.endsWith(".plist")) {
continue;
}
const labelFromName = entry.replace(/\.plist$/, "");
if (isIgnoredLaunchdLabel(labelFromName)) continue;
if (isIgnoredLaunchdLabel(labelFromName)) {
continue;
}
const fullPath = path.join(params.dir, entry);
let contents = "";
try {
@@ -150,7 +170,9 @@ async function scanLaunchdDir(params: {
const label = tryExtractPlistLabel(contents) ?? labelFromName;
if (!marker) {
const legacyLabel = isLegacyLabel(labelFromName) || isLegacyLabel(label);
if (!legacyLabel) continue;
if (!legacyLabel) {
continue;
}
results.push({
platform: "darwin",
label,
@@ -161,8 +183,12 @@ async function scanLaunchdDir(params: {
});
continue;
}
if (isIgnoredLaunchdLabel(label)) continue;
if (marker === "openclaw" && isOpenClawGatewayLaunchdService(label, contents)) continue;
if (isIgnoredLaunchdLabel(label)) {
continue;
}
if (marker === "openclaw" && isOpenClawGatewayLaunchdService(label, contents)) {
continue;
}
results.push({
platform: "darwin",
label,
@@ -189,9 +215,13 @@ async function scanSystemdDir(params: {
}
for (const entry of entries) {
if (!entry.endsWith(".service")) continue;
if (!entry.endsWith(".service")) {
continue;
}
const name = entry.replace(/\.service$/, "");
if (isIgnoredSystemdName(name)) continue;
if (isIgnoredSystemdName(name)) {
continue;
}
const fullPath = path.join(params.dir, entry);
let contents = "";
try {
@@ -200,8 +230,12 @@ async function scanSystemdDir(params: {
continue;
}
const marker = detectMarker(contents);
if (!marker) continue;
if (marker === "openclaw" && isOpenClawGatewaySystemdService(name, contents)) continue;
if (!marker) {
continue;
}
if (marker === "openclaw" && isOpenClawGatewaySystemdService(name, contents)) {
continue;
}
results.push({
platform: "linux",
label: entry,
@@ -234,22 +268,32 @@ function parseSchtasksList(output: string): ScheduledTaskInfo[] {
continue;
}
const idx = line.indexOf(":");
if (idx <= 0) continue;
if (idx <= 0) {
continue;
}
const key = line.slice(0, idx).trim().toLowerCase();
const value = line.slice(idx + 1).trim();
if (!value) continue;
if (!value) {
continue;
}
if (key === "taskname") {
if (current) tasks.push(current);
if (current) {
tasks.push(current);
}
current = { name: value };
continue;
}
if (!current) continue;
if (!current) {
continue;
}
if (key === "task to run") {
current.taskToRun = value;
}
}
if (current) tasks.push(current);
if (current) {
tasks.push(current);
}
return tasks;
}
@@ -290,7 +334,9 @@ export async function findExtraGatewayServices(
const seen = new Set<string>();
const push = (svc: ExtraGatewayService) => {
const key = `${svc.platform}:${svc.label}:${svc.detail}:${svc.scope}`;
if (seen.has(key)) return;
if (seen.has(key)) {
return;
}
seen.add(key);
results.push(svc);
};
@@ -356,14 +402,22 @@ export async function findExtraGatewayServices(
}
if (process.platform === "win32") {
if (!opts.deep) return results;
if (!opts.deep) {
return results;
}
const res = await execSchtasks(["/Query", "/FO", "LIST", "/V"]);
if (res.code !== 0) return results;
if (res.code !== 0) {
return results;
}
const tasks = parseSchtasksList(res.stdout);
for (const task of tasks) {
const name = task.name.trim();
if (!name) continue;
if (isOpenClawGatewayTaskName(name)) continue;
if (!name) {
continue;
}
if (isOpenClawGatewayTaskName(name)) {
continue;
}
const lowerName = name.toLowerCase();
const lowerCommand = task.taskToRun?.toLowerCase() ?? "";
let marker: Marker | null = null;
@@ -373,7 +427,9 @@ export async function findExtraGatewayServices(
break;
}
}
if (!marker) continue;
if (!marker) {
continue;
}
push({
platform: "win32",
label: name,

View File

@@ -17,11 +17,15 @@ const plistUnescape = (value: string): string =>
.replaceAll("&amp;", "&");
const renderEnvDict = (env: Record<string, string | undefined> | undefined): string => {
if (!env) return "";
if (!env) {
return "";
}
const entries = Object.entries(env).filter(
([, value]) => typeof value === "string" && value.trim(),
);
if (entries.length === 0) return "";
if (entries.length === 0) {
return "";
}
const items = entries
.map(
([key, value]) =>
@@ -40,7 +44,9 @@ export async function readLaunchAgentProgramArgumentsFromFile(plistPath: string)
try {
const plist = await fs.readFile(plistPath, "utf8");
const programMatch = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i);
if (!programMatch) return null;
if (!programMatch) {
return null;
}
const args = Array.from(programMatch[1].matchAll(/<string>([\s\S]*?)<\/string>/gi)).map(
(match) => plistUnescape(match[1] ?? "").trim(),
);
@@ -55,7 +61,9 @@ export async function readLaunchAgentProgramArgumentsFromFile(plistPath: string)
/<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/gi,
)) {
const key = plistUnescape(pair[1] ?? "").trim();
if (!key) continue;
if (!key) {
continue;
}
const value = plistUnescape(pair[2] ?? "").trim();
environment[key] = value;
}

View File

@@ -28,7 +28,9 @@ const formatLine = (label: string, value: string) => {
function resolveLaunchAgentLabel(args?: { env?: Record<string, string | undefined> }): string {
const envLabel = args?.env?.OPENCLAW_LAUNCHD_LABEL?.trim();
if (envLabel) return envLabel;
if (envLabel) {
return envLabel;
}
return resolveGatewayLaunchAgentLabel(args?.env?.OPENCLAW_PROFILE);
}
@@ -130,7 +132,9 @@ async function execLaunchctl(
}
function resolveGuiDomain(): string {
if (typeof process.getuid !== "function") return "gui/501";
if (typeof process.getuid !== "function") {
return "gui/501";
}
return `gui/${process.getuid()}`;
}
@@ -145,19 +149,27 @@ export function parseLaunchctlPrint(output: string): LaunchctlPrintInfo {
const entries = parseKeyValueOutput(output, "=");
const info: LaunchctlPrintInfo = {};
const state = entries.state;
if (state) info.state = state;
if (state) {
info.state = state;
}
const pidValue = entries.pid;
if (pidValue) {
const pid = Number.parseInt(pidValue, 10);
if (Number.isFinite(pid)) info.pid = pid;
if (Number.isFinite(pid)) {
info.pid = pid;
}
}
const exitStatusValue = entries["last exit status"];
if (exitStatusValue) {
const status = Number.parseInt(exitStatusValue, 10);
if (Number.isFinite(status)) info.lastExitStatus = status;
if (Number.isFinite(status)) {
info.lastExitStatus = status;
}
}
const exitReason = entries["last exit reason"];
if (exitReason) info.lastExitReason = exitReason;
if (exitReason) {
info.lastExitReason = exitReason;
}
return info;
}
@@ -175,7 +187,9 @@ export async function isLaunchAgentListed(args: {
}): Promise<boolean> {
const label = resolveLaunchAgentLabel({ env: args.env });
const res = await execLaunchctl(["list"]);
if (res.code !== 0) return false;
if (res.code !== 0) {
return false;
}
return res.stdout.split(/\r?\n/).some((line) => line.trim().split(/\s+/).at(-1) === label);
}
@@ -275,7 +289,9 @@ export async function uninstallLegacyLaunchAgents({
}): Promise<LegacyLaunchAgent[]> {
const domain = resolveGuiDomain();
const agents = await findLegacyLaunchAgents(env);
if (agents.length === 0) return agents;
if (agents.length === 0) {
return agents;
}
const home = resolveHomeDir(env);
const trashDir = path.join(home, ".Trash");

View File

@@ -7,15 +7,21 @@ const windowsUncPath = /^\\\\/;
export function resolveHomeDir(env: Record<string, string | undefined>): string {
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
if (!home) throw new Error("Missing HOME");
if (!home) {
throw new Error("Missing HOME");
}
return home;
}
export function resolveUserPathWithHome(input: string, home?: string): string {
const trimmed = input.trim();
if (!trimmed) return trimmed;
if (!trimmed) {
return trimmed;
}
if (trimmed.startsWith("~")) {
if (!home) throw new Error("Missing HOME");
if (!home) {
throw new Error("Missing HOME");
}
const expanded = trimmed.replace(/^~(?=$|[\\/])/, home);
return path.resolve(expanded);
}

View File

@@ -20,7 +20,9 @@ function isBunRuntime(execPath: string): boolean {
async function resolveCliEntrypointPathForService(): Promise<string> {
const argv1 = process.argv[1];
if (!argv1) throw new Error("Unable to resolve CLI entrypoint path");
if (!argv1) {
throw new Error("Unable to resolve CLI entrypoint path");
}
const normalized = path.resolve(argv1);
const resolvedPath = await resolveRealpathSafe(normalized);
@@ -73,7 +75,9 @@ function buildDistCandidates(...inputs: string[]): string[] {
const seen = new Set<string>();
for (const inputPath of inputs) {
if (!inputPath) continue;
if (!inputPath) {
continue;
}
const baseDir = path.dirname(inputPath);
appendDistCandidates(candidates, seen, path.resolve(baseDir, ".."));
appendDistCandidates(candidates, seen, baseDir);
@@ -92,7 +96,9 @@ function appendDistCandidates(candidates: string[], seen: Set<string>, baseDir:
path.join(distDir, "entry.mjs"),
];
for (const entry of distEntries) {
if (seen.has(entry)) continue;
if (seen.has(entry)) {
continue;
}
seen.add(entry);
candidates.push(entry);
}
@@ -105,8 +111,12 @@ function appendNodeModulesBinCandidates(
): void {
const parts = inputPath.split(path.sep);
const binIndex = parts.lastIndexOf(".bin");
if (binIndex <= 0) return;
if (parts[binIndex - 1] !== "node_modules") return;
if (binIndex <= 0) {
return;
}
if (parts[binIndex - 1] !== "node_modules") {
return;
}
const binName = path.basename(inputPath);
const nodeModulesDir = parts.slice(0, binIndex).join(path.sep);
const packageRoot = path.join(nodeModulesDir, binName);
@@ -115,7 +125,9 @@ function appendNodeModulesBinCandidates(
function resolveRepoRootForDev(): string {
const argv1 = process.argv[1];
if (!argv1) throw new Error("Unable to resolve repo root");
if (!argv1) {
throw new Error("Unable to resolve repo root");
}
const normalized = path.resolve(argv1);
const parts = normalized.split(path.sep);
const srcIndex = parts.lastIndexOf("src");
@@ -141,7 +153,9 @@ async function resolveBinaryPath(binary: string): Promise<string> {
try {
const output = execSync(`${cmd} ${binary}`, { encoding: "utf8" }).trim();
const resolved = output.split(/\r?\n/)[0]?.trim();
if (!resolved) throw new Error("empty");
if (!resolved) {
throw new Error("empty");
}
await fs.access(resolved);
return resolved;
} catch {
@@ -252,10 +266,18 @@ export async function resolveNodeProgramArguments(params: {
nodePath?: string;
}): Promise<GatewayProgramArgs> {
const args = ["node", "run", "--host", params.host, "--port", String(params.port)];
if (params.tls || params.tlsFingerprint) args.push("--tls");
if (params.tlsFingerprint) args.push("--tls-fingerprint", params.tlsFingerprint);
if (params.nodeId) args.push("--node-id", params.nodeId);
if (params.displayName) args.push("--display-name", params.displayName);
if (params.tls || params.tlsFingerprint) {
args.push("--tls");
}
if (params.tlsFingerprint) {
args.push("--tls-fingerprint", params.tlsFingerprint);
}
if (params.nodeId) {
args.push("--node-id", params.nodeId);
}
if (params.displayName) {
args.push("--display-name", params.displayName);
}
return resolveCliProgramArguments({
args,
dev: params.dev,

View File

@@ -2,11 +2,17 @@ export function parseKeyValueOutput(output: string, separator: string): Record<s
const entries: Record<string, string> = {};
for (const rawLine of output.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
if (!line) {
continue;
}
const idx = line.indexOf(separator);
if (idx <= 0) continue;
if (idx <= 0) {
continue;
}
const key = line.slice(0, idx).trim().toLowerCase();
if (!key) continue;
if (!key) {
continue;
}
const value = line.slice(idx + separator.length).trim();
entries[key] = value;
}

View File

@@ -24,7 +24,9 @@ describe("resolvePreferredNodePath", () => {
it("uses system node when it meets the minimum version", async () => {
fsMocks.access.mockImplementation(async (target: string) => {
if (target === darwinNode) return;
if (target === darwinNode) {
return;
}
throw new Error("missing");
});
@@ -43,7 +45,9 @@ describe("resolvePreferredNodePath", () => {
it("skips system node when it is too old", async () => {
fsMocks.access.mockImplementation(async (target: string) => {
if (target === darwinNode) return;
if (target === darwinNode) {
return;
}
throw new Error("missing");
});
@@ -82,7 +86,9 @@ describe("resolveSystemNodeInfo", () => {
it("returns supported info when version is new enough", async () => {
fsMocks.access.mockImplementation(async (target: string) => {
if (target === darwinNode) return;
if (target === darwinNode) {
return;
}
throw new Error("missing");
});

View File

@@ -124,7 +124,9 @@ export async function resolveSystemNodeInfo(params: {
const env = params.env ?? process.env;
const platform = params.platform ?? process.platform;
const systemNode = await resolveSystemNodePath(env, platform);
if (!systemNode) return null;
if (!systemNode) {
return null;
}
const version = await resolveNodeVersion(systemNode, params.execFile ?? execFileAsync);
return {
@@ -138,7 +140,9 @@ export function renderSystemNodeWarning(
systemNode: SystemNodeInfo | null,
selectedNodePath?: string,
): string | null {
if (!systemNode || systemNode.supported) return null;
if (!systemNode || systemNode.supported) {
return null;
}
const versionLabel = systemNode.version ?? "unknown";
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.`;
@@ -150,8 +154,12 @@ export async function resolvePreferredNodePath(params: {
platform?: NodeJS.Platform;
execFile?: ExecFileAsync;
}): Promise<string | undefined> {
if (params.runtime !== "node") return undefined;
if (params.runtime !== "node") {
return undefined;
}
const systemNode = await resolveSystemNodeInfo(params);
if (!systemNode?.supported) return undefined;
if (!systemNode?.supported) {
return undefined;
}
return systemNode.path;
}

View File

@@ -18,29 +18,41 @@ const formatLine = (label: string, value: string) => {
function resolveTaskName(env: Record<string, string | undefined>): string {
const override = env.OPENCLAW_WINDOWS_TASK_NAME?.trim();
if (override) return override;
if (override) {
return override;
}
return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE);
}
export function resolveTaskScriptPath(env: Record<string, string | undefined>): string {
const override = env.OPENCLAW_TASK_SCRIPT?.trim();
if (override) return override;
if (override) {
return override;
}
const scriptName = env.OPENCLAW_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
const stateDir = resolveGatewayStateDir(env);
return path.join(stateDir, scriptName);
}
function quoteCmdArg(value: string): string {
if (!/[ \t"]/g.test(value)) return value;
if (!/[ \t"]/g.test(value)) {
return value;
}
return `"${value.replace(/"/g, '\\"')}"`;
}
function resolveTaskUser(env: Record<string, string | undefined>): string | null {
const username = env.USERNAME || env.USER || env.LOGNAME;
if (!username) return null;
if (username.includes("\\")) return username;
if (!username) {
return null;
}
if (username.includes("\\")) {
return username;
}
const domain = env.USERDOMAIN;
if (domain) return `${domain}\\${username}`;
if (domain) {
return `${domain}\\${username}`;
}
return username;
}
@@ -73,7 +85,9 @@ function parseCommandLine(value: string): string[] {
}
current += char;
}
if (current) args.push(current);
if (current) {
args.push(current);
}
return args;
}
@@ -90,16 +104,24 @@ export async function readScheduledTaskCommand(env: Record<string, string | unde
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) {
continue;
}
if (line.startsWith("@echo")) {
continue;
}
if (line.toLowerCase().startsWith("rem ")) {
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;
if (key) {
environment[key] = value;
}
}
continue;
}
@@ -110,7 +132,9 @@ export async function readScheduledTaskCommand(env: Record<string, string | unde
commandLine = line;
break;
}
if (!commandLine) return null;
if (!commandLine) {
return null;
}
return {
programArguments: parseCommandLine(commandLine),
...(workingDirectory ? { workingDirectory } : {}),
@@ -131,11 +155,17 @@ export function parseSchtasksQuery(output: string): ScheduledTaskInfo {
const entries = parseKeyValueOutput(output, ":");
const info: ScheduledTaskInfo = {};
const status = entries.status;
if (status) info.status = status;
if (status) {
info.status = status;
}
const lastRunTime = entries["last run time"];
if (lastRunTime) info.lastRunTime = lastRunTime;
if (lastRunTime) {
info.lastRunTime = lastRunTime;
}
const lastRunResult = entries["last run result"];
if (lastRunResult) info.lastRunResult = lastRunResult;
if (lastRunResult) {
info.lastRunResult = lastRunResult;
}
return info;
}
@@ -159,7 +189,9 @@ function buildTaskScript({
}
if (environment) {
for (const [key, value] of Object.entries(environment)) {
if (!value) continue;
if (!value) {
continue;
}
lines.push(`set ${key}=${value}`);
}
}
@@ -199,7 +231,9 @@ async function execSchtasks(
async function assertSchtasksAvailable() {
const res = await execSchtasks(["/Query"]);
if (res.code === 0) return;
if (res.code === 0) {
return;
}
const detail = res.stderr || res.stdout;
throw new Error(`schtasks unavailable: ${detail || "unknown error"}`.trim());
}

View File

@@ -67,21 +67,35 @@ function parseSystemdUnit(content: string): {
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
if (line.startsWith("#") || line.startsWith(";")) continue;
if (line.startsWith("[")) continue;
if (!line) {
continue;
}
if (line.startsWith("#") || line.startsWith(";")) {
continue;
}
if (line.startsWith("[")) {
continue;
}
const idx = line.indexOf("=");
if (idx <= 0) continue;
if (idx <= 0) {
continue;
}
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (!value) continue;
if (!value) {
continue;
}
if (key === "After") {
for (const entry of value.split(/\s+/)) {
if (entry) after.add(entry);
if (entry) {
after.add(entry);
}
}
} else if (key === "Wants") {
for (const entry of value.split(/\s+/)) {
if (entry) wants.add(entry);
if (entry) {
wants.add(entry);
}
}
} else if (key === "RestartSec") {
restartSec = value;
@@ -92,9 +106,13 @@ function parseSystemdUnit(content: string): {
}
function isRestartSecPreferred(value: string | undefined): boolean {
if (!value) return false;
if (!value) {
return false;
}
const parsed = Number.parseFloat(value);
if (!Number.isFinite(parsed)) return false;
if (!Number.isFinite(parsed)) {
return false;
}
return Math.abs(parsed - 5) < 0.01;
}
@@ -170,7 +188,9 @@ async function auditLaunchdPlist(
}
function auditGatewayCommand(programArguments: string[] | undefined, issues: ServiceConfigIssue[]) {
if (!programArguments || programArguments.length === 0) return;
if (!programArguments || programArguments.length === 0) {
return;
}
if (!hasGatewaySubcommand(programArguments)) {
issues.push({
code: SERVICE_AUDIT_CODES.gatewayCommandMissing,
@@ -209,7 +229,9 @@ function auditGatewayServicePath(
env: Record<string, string | undefined>,
platform: NodeJS.Platform,
) {
if (platform === "win32") return;
if (platform === "win32") {
return;
}
const servicePath = command?.environment?.PATH;
if (!servicePath) {
issues.push({
@@ -276,7 +298,9 @@ async function auditGatewayRuntime(
platform: NodeJS.Platform,
) {
const execPath = command?.programArguments?.[0];
if (!execPath) return;
if (!execPath) {
return;
}
if (isBunRuntime(execPath)) {
issues.push({
@@ -288,7 +312,9 @@ async function auditGatewayRuntime(
return;
}
if (!isNodeRuntime(execPath)) return;
if (!isNodeRuntime(execPath)) {
return;
}
if (isVersionManagedNodePath(execPath, platform)) {
issues.push({

View File

@@ -43,15 +43,21 @@ export function resolveLinuxUserBinDirs(
home: string | undefined,
env?: Record<string, string | undefined>,
): string[] {
if (!home) return [];
if (!home) {
return [];
}
const dirs: string[] = [];
const add = (dir: string | undefined) => {
if (dir) dirs.push(dir);
if (dir) {
dirs.push(dir);
}
};
const appendSubdir = (base: string | undefined, subdir: string) => {
if (!base) return undefined;
if (!base) {
return undefined;
}
return base.endsWith(`/${subdir}`) ? base : path.posix.join(base, subdir);
};
@@ -82,7 +88,9 @@ export function resolveLinuxUserBinDirs(
export function getMinimalServicePathParts(options: MinimalServicePathOptions = {}): string[] {
const platform = options.platform ?? process.platform;
if (platform === "win32") return [];
if (platform === "win32") {
return [];
}
const parts: string[] = [];
const extraDirs = options.extraDirs ?? [];
@@ -93,14 +101,24 @@ export function getMinimalServicePathParts(options: MinimalServicePathOptions =
platform === "linux" ? resolveLinuxUserBinDirs(options.home, options.env) : [];
const add = (dir: string) => {
if (!dir) return;
if (!parts.includes(dir)) parts.push(dir);
if (!dir) {
return;
}
if (!parts.includes(dir)) {
parts.push(dir);
}
};
for (const dir of extraDirs) add(dir);
for (const dir of extraDirs) {
add(dir);
}
// User dirs first so user-installed binaries take precedence
for (const dir of linuxUserDirs) add(dir);
for (const dir of systemDirs) add(dir);
for (const dir of linuxUserDirs) {
add(dir);
}
for (const dir of systemDirs) {
add(dir);
}
return parts;
}

View File

@@ -1,7 +1,9 @@
import { formatCliCommand } from "../cli/command-format.js";
export function isSystemdUnavailableDetail(detail?: string): boolean {
if (!detail) return false;
if (!detail) {
return false;
}
const normalized = detail.toLowerCase();
return (
normalized.includes("systemctl --user unavailable") ||

View File

@@ -3,7 +3,9 @@ import { runCommandWithTimeout, runExec } from "../process/exec.js";
function resolveLoginctlUser(env: Record<string, string | undefined>): string | null {
const fromEnv = env.USER?.trim() || env.LOGNAME?.trim();
if (fromEnv) return fromEnv;
if (fromEnv) {
return fromEnv;
}
try {
return os.userInfo().username;
} catch {
@@ -20,7 +22,9 @@ export async function readSystemdUserLingerStatus(
env: Record<string, string | undefined>,
): Promise<SystemdUserLingerStatus | null> {
const user = resolveLoginctlUser(env);
if (!user) return null;
if (!user) {
return null;
}
try {
const { stdout } = await runExec("loginctl", ["show-user", user, "-p", "Linger"], {
timeoutMs: 5_000,

View File

@@ -1,14 +1,20 @@
function systemdEscapeArg(value: string): string {
if (!/[\\s"\\\\]/.test(value)) return value;
if (!/[\\s"\\\\]/.test(value)) {
return value;
}
return `"${value.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"')}"`;
}
function renderEnvLines(env: Record<string, string | undefined> | undefined): string[] {
if (!env) return [];
if (!env) {
return [];
}
const entries = Object.entries(env).filter(
([, value]) => typeof value === "string" && value.trim(),
);
if (entries.length === 0) return [];
if (entries.length === 0) {
return [];
}
return entries.map(
([key, value]) => `Environment=${systemdEscapeArg(`${key}=${value?.trim() ?? ""}`)}`,
);
@@ -85,16 +91,22 @@ export function parseSystemdExecStart(value: string): string[] {
}
current += char;
}
if (current) args.push(current);
if (current) {
args.push(current);
}
return args;
}
export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null {
const trimmed = raw.trim();
if (!trimmed) return null;
if (!trimmed) {
return null;
}
const unquoted = (() => {
if (!(trimmed.startsWith('"') && trimmed.endsWith('"'))) return trimmed;
if (!(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
return trimmed;
}
let out = "";
let escapeNext = false;
for (const ch of trimmed.slice(1, -1)) {
@@ -113,9 +125,13 @@ export function parseSystemdEnvAssignment(raw: string): { key: string; value: st
})();
const eq = unquoted.indexOf("=");
if (eq <= 0) return null;
if (eq <= 0) {
return null;
}
const key = unquoted.slice(0, eq).trim();
if (!key) return null;
if (!key) {
return null;
}
const value = unquoted.slice(eq + 1);
return { key, value };
}

View File

@@ -75,7 +75,9 @@ export async function readSystemdServiceExecStart(
const environment: Record<string, string> = {};
for (const rawLine of content.split("\n")) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
if (!line || line.startsWith("#")) {
continue;
}
if (line.startsWith("ExecStart=")) {
execStart = line.slice("ExecStart=".length).trim();
} else if (line.startsWith("WorkingDirectory=")) {
@@ -83,10 +85,14 @@ export async function readSystemdServiceExecStart(
} else if (line.startsWith("Environment=")) {
const raw = line.slice("Environment=".length).trim();
const parsed = parseSystemdEnvAssignment(raw);
if (parsed) environment[parsed.key] = parsed.value;
if (parsed) {
environment[parsed.key] = parsed.value;
}
}
}
if (!execStart) return null;
if (!execStart) {
return null;
}
const programArguments = parseSystemdExecStart(execStart);
return {
programArguments,
@@ -111,21 +117,31 @@ export function parseSystemdShow(output: string): SystemdServiceInfo {
const entries = parseKeyValueOutput(output, "=");
const info: SystemdServiceInfo = {};
const activeState = entries.activestate;
if (activeState) info.activeState = activeState;
if (activeState) {
info.activeState = activeState;
}
const subState = entries.substate;
if (subState) info.subState = subState;
if (subState) {
info.subState = subState;
}
const mainPidValue = entries.mainpid;
if (mainPidValue) {
const pid = Number.parseInt(mainPidValue, 10);
if (Number.isFinite(pid) && pid > 0) info.mainPid = pid;
if (Number.isFinite(pid) && pid > 0) {
info.mainPid = pid;
}
}
const execMainStatusValue = entries.execmainstatus;
if (execMainStatusValue) {
const status = Number.parseInt(execMainStatusValue, 10);
if (Number.isFinite(status)) info.execMainStatus = status;
if (Number.isFinite(status)) {
info.execMainStatus = status;
}
}
const execMainCode = entries.execmaincode;
if (execMainCode) info.execMainCode = execMainCode;
if (execMainCode) {
info.execMainCode = execMainCode;
}
return info;
}
@@ -159,20 +175,36 @@ async function execSystemctl(
export async function isSystemdUserServiceAvailable(): Promise<boolean> {
const res = await execSystemctl(["--user", "status"]);
if (res.code === 0) return true;
if (res.code === 0) {
return true;
}
const detail = `${res.stderr} ${res.stdout}`.toLowerCase();
if (!detail) return false;
if (detail.includes("not found")) return false;
if (detail.includes("failed to connect")) return false;
if (detail.includes("not been booted")) return false;
if (detail.includes("no such file or directory")) return false;
if (detail.includes("not supported")) return false;
if (!detail) {
return false;
}
if (detail.includes("not found")) {
return false;
}
if (detail.includes("failed to connect")) {
return false;
}
if (detail.includes("not been booted")) {
return false;
}
if (detail.includes("no such file or directory")) {
return false;
}
if (detail.includes("not supported")) {
return false;
}
return false;
}
async function assertSystemdAvailable() {
const res = await execSystemctl(["--user", "status"]);
if (res.code === 0) return;
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.");
@@ -352,7 +384,9 @@ export type LegacySystemdUnit = {
async function isSystemctlAvailable(): Promise<boolean> {
const res = await execSystemctl(["--user", "status"]);
if (res.code === 0) return true;
if (res.code === 0) {
return true;
}
const detail = `${res.stderr || res.stdout}`.toLowerCase();
return !detail.includes("not found");
}
@@ -391,7 +425,9 @@ export async function uninstallLegacySystemdUnits({
stdout: NodeJS.WritableStream;
}): Promise<LegacySystemdUnit[]> {
const units = await findLegacySystemdUnits(env);
if (units.length === 0) return units;
if (units.length === 0) {
return units;
}
const systemctlAvailable = await isSystemctlAvailable();
for (const unit of units) {